From ea8efba265db647ea94ffb4d60412902bb534d4c Mon Sep 17 00:00:00 2001
From: Taslan Graham <gtaslan1@gmail.com>
Date: Thu, 7 Nov 2024 22:49:44 -0500
Subject: [PATCH] pkp/pkp-lib#10571 WIP: add support for unrestricted templates

---
 .../PKPEmailTemplateController.php            |  9 ++++
 .../forms/FieldEmailTemplateUnrestricted.php  | 33 +++++++++++++++
 .../forms/emailTemplate/EmailTemplateForm.php | 12 +++++-
 classes/decision/steps/Email.php              |  7 ++++
 classes/emailTemplate/DAO.php                 | 26 ------------
 classes/emailTemplate/Repository.php          | 41 ++++++++++++++++---
 classes/emailTemplate/maps/Schema.php         |  2 +-
 classes/mail/Repository.php                   |  7 +---
 schemas/emailTemplate.json                    |  5 +++
 9 files changed, 102 insertions(+), 40 deletions(-)
 create mode 100644 classes/components/forms/FieldEmailTemplateUnrestricted.php

diff --git a/api/v1/emailTemplates/PKPEmailTemplateController.php b/api/v1/emailTemplates/PKPEmailTemplateController.php
index b0bfce34567..ea8dfb34d0a 100644
--- a/api/v1/emailTemplates/PKPEmailTemplateController.php
+++ b/api/v1/emailTemplates/PKPEmailTemplateController.php
@@ -201,6 +201,10 @@ public function add(Request $illuminateRequest): JsonResponse
 
         $emailTemplate = Repo::emailTemplate()->newDataObject($params);
         Repo::emailTemplate()->add($emailTemplate);
+
+        if($params['userGroupIds']) {
+            Repo::emailTemplate()->updateTemplateAccessGroups($emailTemplate, $params['userGroupIds'], $requestContext->getId());
+        }
         $emailTemplate = Repo::emailTemplate()->getByKey($emailTemplate->getData('contextId'), $emailTemplate->getData('key'));
 
         return response()->json(Repo::emailTemplate()->getSchemaMap()->map($emailTemplate), Response::HTTP_OK);
@@ -238,6 +242,11 @@ public function edit(Request $illuminateRequest): JsonResponse
             $params['contextId'] = $requestContext->getId();
         }
 
+
+        // If the user submitted an empty list (meaning all user groups were unchecked), the empty array is converted to null in the request's data.
+        // Convert back to an empty array.
+        $params['userGroupIds'] = $params['userGroupIds'] ?: [];
+
         $errors = Repo::emailTemplate()->validate(
             $emailTemplate,
             $params,
diff --git a/classes/components/forms/FieldEmailTemplateUnrestricted.php b/classes/components/forms/FieldEmailTemplateUnrestricted.php
new file mode 100644
index 00000000000..80e62146b57
--- /dev/null
+++ b/classes/components/forms/FieldEmailTemplateUnrestricted.php
@@ -0,0 +1,33 @@
+<?php
+
+/**
+ * @file classes/components/form/FieldEmailTemplateUnrestricted.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 FieldEmailTemplateUnrestricted
+ *
+ * @ingroup classes_controllers_form
+ *
+ * @brief A component to indicate if an email template is unrestricted, i.e accessible to all user groups within the associated mailable
+ */
+
+namespace PKP\components\forms;
+
+class FieldEmailTemplateUnrestricted extends Field
+{
+    /** @copydoc Field::$component */
+    public $component = 'field-email-template-unrestricted';
+
+    /**
+     * @copydoc Field::getConfig()
+     */
+    public function getConfig()
+    {
+        $config = parent::getConfig();
+        $config['label'] = $this->label;
+        return $config;
+    }
+}
diff --git a/classes/components/forms/emailTemplate/EmailTemplateForm.php b/classes/components/forms/emailTemplate/EmailTemplateForm.php
index 7e42bd2c211..9011049e67d 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\FieldEmailTemplateUnrestricted;
 use PKP\components\forms\FieldEmailTemplateUserGroupSettings;
 use PKP\components\forms\FieldPreparedContent;
 use PKP\components\forms\FieldText;
@@ -47,9 +48,16 @@ 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', [
+            ]))->addField(
+                new FieldEmailTemplateUnrestricted('isUnrestricted'),
+                [
+                    'type' => 'checkbox'
+                ]
+            )
+            ->addField(new FieldEmailTemplateUserGroupSettings('userGroupIds', [
+                'type' => 'checkbox',
                 'label' => __('workflow.userGroup.allowed'),
-                'type' => 'checkbox'
+
             ]));
     }
 }
diff --git a/classes/decision/steps/Email.php b/classes/decision/steps/Email.php
index ab3200d6a9d..f8114728531 100644
--- a/classes/decision/steps/Email.php
+++ b/classes/decision/steps/Email.php
@@ -21,6 +21,7 @@
 use PKP\facades\Locale;
 use PKP\mail\Mailable;
 use PKP\user\User;
+use Role;
 use stdClass;
 
 class Email extends Step
@@ -124,6 +125,12 @@ protected function getEmailTemplates(): array
     {
         $request = Application::get()->getRequest();
         $context = $request->getContext();
+        $userRoles = array_map(fn (Role $role) => $role->getId(), $request->getUser()->getRoles($context->getId()));
+
+        // Ensure user has access to mailable before proceeding
+        if(empty(array_intersect($this->mailable::getFromRoleIds(), $userRoles))) {
+            return [];
+        }
 
         $emailTemplates = collect();
         if ($this->mailable::getEmailTemplateKey()) {
diff --git a/classes/emailTemplate/DAO.php b/classes/emailTemplate/DAO.php
index 66e1587c990..828185ec303 100644
--- a/classes/emailTemplate/DAO.php
+++ b/classes/emailTemplate/DAO.php
@@ -450,31 +450,5 @@ protected function getUniqueKey(EmailTemplate $emailTemplate): string
     }
 
 
-    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 41f1a2790c3..a6b40745de8 100644
--- a/classes/emailTemplate/Repository.php
+++ b/classes/emailTemplate/Repository.php
@@ -172,11 +172,12 @@ public function add(EmailTemplate $emailTemplate): string
     /** @copydoc DAO::update() */
     public function edit(EmailTemplate $emailTemplate, array $params, $contextId)
     {
+        $userGroupIds = $params['userGroupIds'];
+        unset($params['userGroupIds']);
         $newEmailTemplate = clone $emailTemplate;
         $newEmailTemplate->setAllData(array_merge($newEmailTemplate->_data, $params));
 
-        $userGroupIds = $params['userGroupIds'];
-        unset($params['userGroupIds']);
+
 
         Hook::call('EmailTemplate::edit', [$newEmailTemplate, $emailTemplate, $params]);
 
@@ -186,9 +187,7 @@ public function edit(EmailTemplate $emailTemplate, array $params, $contextId)
             $this->dao->insert($newEmailTemplate);
         }
 
-        if($userGroupIds) {
-            $this->dao->updateTemplateAccessGroups($emailTemplate, $userGroupIds, $contextId);
-        }
+        $this->updateTemplateAccessGroups($emailTemplate, $params['userGroupIds'], $contextId);
     }
 
     /** @copydoc DAO::delete() */
@@ -278,4 +277,36 @@ public function filterTemplatesByUserAccess(array $templates, User $user, int $c
         return collect(array_filter($templates, fn (EmailTemplate $template) => $this->isTemplateAccessibleToUser($user, $template, $contextId)));
     }
 
+    /**
+     * Pass empty array to delete all existing user groups for a template
+     */
+    public function updateTemplateAccessGroups(EmailTemplate $emailTemplate, array $newUserGroupIds, int $contextId): void
+    {
+        $isUnrestricted = in_array(null, $newUserGroupIds);
+        // remove any null values
+        // 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/maps/Schema.php b/classes/emailTemplate/maps/Schema.php
index 299dcb292c0..595871486c8 100644
--- a/classes/emailTemplate/maps/Schema.php
+++ b/classes/emailTemplate/maps/Schema.php
@@ -77,7 +77,7 @@ public function summarizeMany(Enumerable $collection, string $mailableClass = nu
     }
 
     /**
-     * Map schema properties of an Announcement to an assoc array
+     * Map schema properties of an Email Template to an assoc array
      */
     protected function mapByProperties(array $props, EmailTemplate $item, string $mailableClass = null): array
     {
diff --git a/classes/mail/Repository.php b/classes/mail/Repository.php
index 4f69043a989..36d2f02d412 100644
--- a/classes/mail/Repository.php
+++ b/classes/mail/Repository.php
@@ -229,12 +229,7 @@ protected function isMailableConfigurable(string $class, Context $context): bool
  */
     public function isGroupsAssignableToTemplates(Mailable|string $mailable): bool
     {
-        return !empty(array_intersect($mailable::getGroupIds(), [
-            Mailable::GROUP_SUBMISSION,
-            Mailable::GROUP_REVIEW,
-            Mailable::GROUP_COPYEDITING,
-            Mailable::GROUP_PRODUCTION,
-        ]));
+        return !in_array(Mailable::FROM_SYSTEM, $mailable::getFromRoleIds());
     }
 
     /**
diff --git a/schemas/emailTemplate.json b/schemas/emailTemplate.json
index e496a69b166..017be1187ba 100644
--- a/schemas/emailTemplate.json
+++ b/schemas/emailTemplate.json
@@ -68,6 +68,11 @@
 			"validation": [
 				"nullable"
 			]
+		},
+		"isUnrestricted": {
+			"type": "boolean",
+			"description": "Boolean indicating  if an email template is available to all user groups within the roles associated with the template's mailable",
+			"apiSummary": true
 		}
 	}
 }