From f42a6ba99b2e911e3bc2fc1348c3ad946daf7f11 Mon Sep 17 00:00:00 2001
From: Taslan Graham <gtaslan1@gmail.com>
Date: Fri, 8 Nov 2024 20:57:59 -0500
Subject: [PATCH] pkp/pkp-lib#10571 Convert queries to eloquent

---
 .../PKPEmailTemplateController.php            |   6 +-
 .../EmailTemplateAccessGroup.php              |  34 ++++
 classes/emailTemplate/Repository.php          | 150 ++++++++++++------
 classes/emailTemplate/maps/Schema.php         |  11 +-
 .../v3_5_0/I10403_EmailTemplateRoleAccess.php |   8 +-
 5 files changed, 152 insertions(+), 57 deletions(-)
 create mode 100644 classes/emailTemplate/EmailTemplateAccessGroup.php

diff --git a/api/v1/emailTemplates/PKPEmailTemplateController.php b/api/v1/emailTemplates/PKPEmailTemplateController.php
index 82c99283fb7..c8104bd5a9e 100644
--- a/api/v1/emailTemplates/PKPEmailTemplateController.php
+++ b/api/v1/emailTemplates/PKPEmailTemplateController.php
@@ -205,9 +205,8 @@ 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());
-        }
+        Repo::emailTemplate()->setEmailTemplateAccess($emailTemplate, $requestContext->getId(), $params['userGroupIds'], $params['isUnrestricted']);
+
         $emailTemplate = Repo::emailTemplate()->getByKey($emailTemplate->getData('contextId'), $emailTemplate->getData('key'));
 
         return response()->json(Repo::emailTemplate()->getSchemaMap()->map($emailTemplate), Response::HTTP_OK);
@@ -261,6 +260,7 @@ public function edit(Request $illuminateRequest): JsonResponse
         }
 
         Repo::emailTemplate()->edit($emailTemplate, $params, $requestContext->getId());
+        Repo::emailTemplate()->setEmailTemplateAccess($emailTemplate, $requestContext->getId(), $params['userGroupIds'], $params['isUnrestricted']);
 
         $emailTemplate = Repo::emailTemplate()->getByKey(
             // context ID is null if edited for the first time
diff --git a/classes/emailTemplate/EmailTemplateAccessGroup.php b/classes/emailTemplate/EmailTemplateAccessGroup.php
new file mode 100644
index 00000000000..bb139a53f9a
--- /dev/null
+++ b/classes/emailTemplate/EmailTemplateAccessGroup.php
@@ -0,0 +1,34 @@
+<?php
+
+namespace PKP\emailTemplate;
+
+use Eloquence\Behaviours\HasCamelCasing;
+use Eloquence\Database\Model;
+use Illuminate\Contracts\Database\Eloquent\Builder;
+
+class EmailTemplateAccessGroup extends Model
+{
+    use HasCamelCasing;
+    public $timestamps = false;
+    protected $primaryKey = 'email_template_user_group_access_id';
+    protected $table = 'email_template_user_group_access';
+    protected $fillable = ['userGroupId', 'contextId','emailKey'];
+
+
+    public function scopeWithEmailKey(Builder $query, ?array $keys): Builder
+    {
+        return $query->when(!empty($keys), function ($query) use ($keys) {
+            return $query->whereIn('email_key', $keys);
+        });
+    }
+
+    public function scopeWithContextId(Builder $query, int $contextId): Builder
+    {
+        return $query->where('context_id', $contextId);
+    }
+
+    public function scopeWithGroupIds(Builder $query, array $ids): Builder
+    {
+        return $query->whereIn('user_group_id', $ids);
+    }
+}
diff --git a/classes/emailTemplate/Repository.php b/classes/emailTemplate/Repository.php
index ba99b90106a..910a618aebc 100644
--- a/classes/emailTemplate/Repository.php
+++ b/classes/emailTemplate/Repository.php
@@ -15,8 +15,8 @@
 
 use APP\emailTemplate\DAO;
 use APP\facades\Repo;
+use Illuminate\Support\Collection;
 use Illuminate\Support\Enumerable;
-use Illuminate\Support\Facades\DB;
 use PKP\context\Context;
 use PKP\core\PKPRequest;
 use PKP\plugins\Hook;
@@ -173,13 +173,9 @@ 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));
 
-
-
         Hook::call('EmailTemplate::edit', [$newEmailTemplate, $emailTemplate, $params]);
 
         if ($newEmailTemplate->getId()) {
@@ -187,8 +183,6 @@ public function edit(EmailTemplate $emailTemplate, array $params, $contextId)
         } else {
             $this->dao->insert($newEmailTemplate);
         }
-
-        $this->updateTemplateAccessGroups($emailTemplate, $userGroupIds, $contextId);
     }
 
     /** @copydoc DAO::delete() */
@@ -234,17 +228,34 @@ public function restoreDefaults($contextId): array
     }
 
 
-    public function getGroupsAssignedToTemplate(string $key, int $contextId): array
+    /***
+     * Gets the IDs of the user groups assigned to an email template
+     */
+    public function getUserGroupsIdsAssignedToTemplate(string $templateKey, int $contextId): array
+    {
+        return EmailTemplateAccessGroup::withEmailKey([$templateKey])
+            ->withContextId($contextId)
+            ->whereNot('user_group_id', null)
+            ->get()
+            ->pluck('userGroupId')
+            ->all();
+    }
+
+    /***
+     * Checks if an Email Template is unrestricted
+     */
+    public function isTemplateUnrestricted(string $templateKey, int $contextId): bool
     {
-        // FIXME - can this be replaced with eloquent?
-        return DB::table('email_template_role_access')
-            ->where('email_key', $key)
-            ->where('context_id', $contextId)
-            ->pluck('user_group_id')
-            ->toArray();
+        return !!EmailTemplateAccessGroup::withEmailKey([$templateKey])
+            ->withContextId($contextId)
+            ->where('user_group_id', null)
+            ->first();
     }
 
 
+    /**
+     * Checks if an email template is accessible to a user. A template is accessible if it is assigned to a user group that the user belongs to or if the template is unrestricted
+     */
     public function isTemplateAccessibleToUser(User $user, EmailTemplate $template, int $contextId): bool
     {
         if ($user->hasRole([Role::ROLE_ID_SITE_ADMIN, Role::ROLE_ID_MANAGER,], $contextId)) {
@@ -252,17 +263,21 @@ public function isTemplateAccessibleToUser(User $user, EmailTemplate $template,
         }
 
         $userUserGroups = Repo::userGroup()->userUserGroups($user->getId(), $contextId)->all();
-        $templateUserGroups = $this->getGroupsAssignedToTemplate($template->getData('key'), $contextId);
-        $userHasAccess = false;
+        $templateUserGroups = $this->getUserGroupsIdsAssignedToTemplate($template->getData('key'), $contextId);
+
+        // Null entry indicates that template is unrestricted
+        if(in_array(null, $templateUserGroups)) {
+            return true;
+        }
+
 
         foreach ($userUserGroups as $userGroup) {
-            if (in_array($userGroup->getId(), $templateUserGroups) || $template->getData('isUnrestricted')) {
-                $userHasAccess = true;
-                break;
+            if (in_array($userGroup->getId(), $templateUserGroups)) {
+                return true;
             }
         }
 
-        return $userHasAccess;
+        return false;
     }
 
     /**
@@ -271,9 +286,9 @@ public function isTemplateAccessibleToUser(User $user, EmailTemplate $template,
      * @param Enumerable $templates List of EmailTemplate objects to filter.
      * @param User $user The user whose access level is used for filtering.
      *
-     * @return \Illuminate\Support\Collection Filtered list of EmailTemplate objects accessible to the user.
+     * @return Collection Filtered list of EmailTemplate objects accessible to the user.
      */
-    public function filterTemplatesByUserAccess(Enumerable $templates, User $user, int $contextId): \Illuminate\Support\Collection
+    public function filterTemplatesByUserAccess(Enumerable $templates, User $user, int $contextId): Collection
     {
         $filteredTemplates = collect();
 
@@ -286,36 +301,79 @@ public function filterTemplatesByUserAccess(Enumerable $templates, User $user, i
         return $filteredTemplates;
     }
 
-    /**
-     * Pass empty array to delete all existing user groups for a template
+    /***
+     * Internal method used to assign user group IDs to an email template
      */
-    public function updateTemplateAccessGroups(EmailTemplate $emailTemplate, array $newUserGroupIds, int $contextId): void
+    private 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();
+        EmailTemplateAccessGroup::withEmailKey([$emailTemplate->getData('key')])
+            ->withContextId($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,
-                    ]
-                );
+            EmailTemplateAccessGroup::updateOrCreate(
+                [
+                    // 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)
+                    'emailKey' => $emailTemplate->getData('key'),
+                    'userGroupId' => $id,
+                    'contextId' => $contextId,
+                ]
+            );
         }
+    }
 
+    /**
+     * Pass empty array in $userGroupIds to delete all existing user groups for a template
+     */
+    public function setEmailTemplateAccess(EmailTemplate $emailTemplate, int $contextId, ?array $userGroupIds, ?bool $isUnrestricted): void
+    {
+        if($userGroupIds !== null) {
+            $this->_updateTemplateAccessGroups($emailTemplate, $userGroupIds, $contextId);
+        }
+
+        if($isUnrestricted !== null) {
+            $this->markTemplateAsUnrestricted($emailTemplate, $isUnrestricted, $contextId);
+        }
+    }
+
+
+    /**
+     * Mark an email template as unrestricted or not.
+     * An unrestricted email template is available to all user groups associated with the Roles linked to the mailable that the template belongs to.
+     * Mailable roles are stored in the $fromRoleIds property of a mailable
+     */
+    private function markTemplateAsUnrestricted(EmailTemplate $emailTemplate, bool $isUnrestricted, int $contextId): void
+    {
+        // Unrestricted emails are represented by an entry with a `null` value for the user group ID
+        if ($isUnrestricted) {
+            EmailTemplateAccessGroup::updateOrCreate(
+                [
+                    // The where conditions (keys that should match)
+                    'email_key' => $emailTemplate->getData('key'),
+                    'user_group_id' => null,
+                    'context_id' => $contextId,
+                ],
+                [
+                    // The data to insert or update (values to set)
+                    'emailKey' => $emailTemplate->getData('key'),
+                    'userGroupId' => null,
+                    'contextId' => $contextId,
+                ]
+            );
+
+        } else {
+            // Remove entry with a `null` value for the user group ID to reflect that it is no longer unrestricted
+            EmailTemplateAccessGroup::withEmailKey([$emailTemplate->getData('key')])
+                ->withContextId($contextId)
+                ->withGroupIds([null])
+                ->delete();
+        }
     }
 
 }
diff --git a/classes/emailTemplate/maps/Schema.php b/classes/emailTemplate/maps/Schema.php
index a402ee64afd..0df97f706ae 100644
--- a/classes/emailTemplate/maps/Schema.php
+++ b/classes/emailTemplate/maps/Schema.php
@@ -84,16 +84,16 @@ protected function mapByProperties(array $props, EmailTemplate $item, string $ma
         $output = [];
 
         $mailableClass = $mailableClass ?? Repo::mailable()->getMailableByEmailTemplate($item);
-
+        $assignedUserGroupsIds = [];
         // some mailable are not found during some operations such as performing a search for templates. So ensure mailable exist before using
-        if($mailableClass) {
+        if ($mailableClass) {
             $isUserGroupsAssignable = Repo::mailable()->isGroupsAssignableToTemplates($mailableClass);
-
+            $assignedUserGroupsIds = Repo::emailTemplate()->getUserGroupsIdsAssignedToTemplate($item->getData('key'), Application::get()->getRequest()->getContext()->getId());
             if (!$isUserGroupsAssignable) {
                 $output['assignedUserGroupIds'] = [];
             } else {
                 // Get the current user groups assigned to the template
-                $output['assignedUserGroupIds'] = Repo::emailTemplate()->getGroupsAssignedToTemplate($item->getData('key'), Application::get()->getRequest()->getContext()->getId());
+                $output['assignedUserGroupIds'] = $assignedUserGroupsIds;
             }
         }
 
@@ -108,6 +108,9 @@ protected function mapByProperties(array $props, EmailTemplate $item, string $ma
                         'emailTemplates/' . $item->getData('key')
                     );
                     break;
+                case 'isUnrestricted':
+                    $output['isUnrestricted'] = Repo::emailTemplate()->isTemplateUnrestricted($item->getData('key'), Application::get()->getRequest()->getContext()->getId());
+                    break;
                 default:
                     $output[$prop] = $item->getData($prop);
                     break;
diff --git a/classes/migration/upgrade/v3_5_0/I10403_EmailTemplateRoleAccess.php b/classes/migration/upgrade/v3_5_0/I10403_EmailTemplateRoleAccess.php
index 8662b51de88..cbfc1186e74 100644
--- a/classes/migration/upgrade/v3_5_0/I10403_EmailTemplateRoleAccess.php
+++ b/classes/migration/upgrade/v3_5_0/I10403_EmailTemplateRoleAccess.php
@@ -14,11 +14,11 @@ class I10403_EmailTemplateRoleAccess extends Migration
     public function up(): void
     {
 
-        Schema::create('email_template_role_access', function (Blueprint $table) {
-            $table->bigInteger('email_template_role_access_id')->autoIncrement();
+        Schema::create('email_template_user_group_access', function (Blueprint $table) {
+            $table->bigInteger('email_template_user_group_access_id')->autoIncrement();
             $table->string('email_key', 255);
             $table->bigInteger('context_id');
-            $table->bigInteger('user_group_id');
+            $table->bigInteger('user_group_id')->nullable();
 
             $table->foreign('context_id')->references('journal_id')->on('journals')->onDelete('cascade');
             $table->foreign('user_group_id')->references('user_group_id')->on('user_groups')->onDelete('cascade');
@@ -30,6 +30,6 @@ public function up(): void
      */
     public function down(): void
     {
-        Schema::drop('email_template_role_access');
+        Schema::drop('email_template_user_group_access');
     }
 }