Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
0.00% covered (danger)
0.00%
0 / 156
0.00% covered (danger)
0.00%
0 / 9
CRAP
0.00% covered (danger)
0.00%
0 / 1
EmailTemplatesController
0.00% covered (danger)
0.00%
0 / 156
0.00% covered (danger)
0.00%
0 / 9
812
0.00% covered (danger)
0.00%
0 / 1
 index
0.00% covered (danger)
0.00%
0 / 29
0.00% covered (danger)
0.00%
0 / 1
12
 view
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
2
 add
0.00% covered (danger)
0.00%
0 / 11
0.00% covered (danger)
0.00%
0 / 1
12
 edit
0.00% covered (danger)
0.00%
0 / 11
0.00% covered (danger)
0.00%
0 / 1
12
 htmlToPlainText
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
2
 delete
0.00% covered (danger)
0.00%
0 / 6
0.00% covered (danger)
0.00%
0 / 1
6
 sendEmail
0.00% covered (danger)
0.00%
0 / 59
0.00% covered (danger)
0.00%
0 / 1
30
 prepareEmailVariables
0.00% covered (danger)
0.00%
0 / 12
0.00% covered (danger)
0.00%
0 / 1
30
 generateLink
0.00% covered (danger)
0.00%
0 / 23
0.00% covered (danger)
0.00%
0 / 1
30
1<?php
2declare(strict_types=1);
3
4namespace App\Controller\Admin;
5
6use App\Controller\AppController;
7use App\Model\Entity\User;
8use Cake\Http\Response;
9use Cake\Log\Log;
10use Cake\Mailer\Mailer;
11use Cake\ORM\TableRegistry;
12use Cake\Routing\Router;
13use Cake\Utility\Text;
14use Exception;
15
16/**
17 * EmailTemplates Controller
18 *
19 * Manages email templates and sending emails based on these templates.
20 *
21 * @property \App\Model\Table\EmailTemplatesTable $EmailTemplates
22 */
23class EmailTemplatesController extends AppController
24{
25    /**
26     * Displays a paginated list of email templates.
27     *
28     * @return void
29     */
30    public function index(): ?Response
31    {
32        $query = $this->EmailTemplates->find()
33            ->select([
34                'EmailTemplates.id',
35                'EmailTemplates.template_identifier',
36                'EmailTemplates.name',
37                'EmailTemplates.subject',
38                'EmailTemplates.body_html',
39                'EmailTemplates.body_plain',
40                'EmailTemplates.created',
41                'EmailTemplates.modified',
42            ]);
43
44        $search = $this->request->getQuery('search');
45        if (!empty($search)) {
46            $query->where([
47                'OR' => [
48                    'EmailTemplates.template_identifier LIKE' => '%' . $search . '%',
49                    'EmailTemplates.name LIKE' => '%' . $search . '%',
50                    'EmailTemplates.subject LIKE' => '%' . $search . '%',
51                    'EmailTemplates.body_html LIKE' => '%' . $search . '%',
52                    'EmailTemplates.body_plain LIKE' => '%' . $search . '%',
53                ],
54            ]);
55        }
56        $emailTemplates = $this->paginate($query);
57        if ($this->request->is('ajax')) {
58            $this->set(compact('emailTemplates', 'search'));
59            $this->viewBuilder()->setLayout('ajax');
60
61            return $this->render('search_results');
62        }
63        $this->set(compact('emailTemplates'));
64
65        return null;
66    }
67
68    /**
69     * Displays details of a specific email template.
70     *
71     * @param string|null $id Email Template id.
72     * @return void
73     * @throws \Cake\Datasource\Exception\RecordNotFoundException When record not found.
74     */
75    public function view(?string $id = null): void
76    {
77        $emailTemplate = $this->EmailTemplates->get($id, contain: []);
78        $this->set(compact('emailTemplate'));
79    }
80
81    /**
82     * Adds a new email template.
83     *
84     * @return \Cake\Http\Response|null Redirects on successful add, renders view otherwise.
85     */
86    public function add(): ?Response
87    {
88        $emailTemplate = $this->EmailTemplates->newEmptyEntity();
89        if ($this->request->is('post')) {
90            $data = $this->request->getData();
91            $data['body_plain'] = $this->htmlToPlainText($data['body_html']);
92
93            $emailTemplate = $this->EmailTemplates->patchEntity($emailTemplate, $data);
94            if ($this->EmailTemplates->save($emailTemplate)) {
95                $this->Flash->success(__('The email template has been saved.'));
96
97                return $this->redirect(['action' => 'index']);
98            }
99            $this->Flash->error(__('The email template could not be saved. Please, try again.'));
100        }
101        $this->set(compact('emailTemplate'));
102
103        return null;
104    }
105
106    /**
107     * Edits an existing email template.
108     *
109     * @param string|null $id Email Template id.
110     * @return \Cake\Http\Response|null Redirects on successful edit, renders view otherwise.
111     * @throws \Cake\Datasource\Exception\RecordNotFoundException When record not found.
112     */
113    public function edit(?string $id = null): ?Response
114    {
115        $emailTemplate = $this->EmailTemplates->get($id, contain: []);
116        if ($this->request->is(['patch', 'post', 'put'])) {
117            $data = $this->request->getData();
118            $data['body_plain'] = $this->htmlToPlainText($data['body_html']);
119
120            $emailTemplate = $this->EmailTemplates->patchEntity($emailTemplate, $data);
121            if ($this->EmailTemplates->save($emailTemplate)) {
122                $this->Flash->success(__('The email template has been saved.'));
123
124                return $this->redirect(['action' => 'index']);
125            }
126            $this->Flash->error(__('The email template could not be saved. Please, try again.'));
127        }
128        $this->set(compact('emailTemplate'));
129
130        return null;
131    }
132
133    /**
134     * Converts HTML content to plain text.
135     *
136     * @param string $html The HTML content to be converted.
137     * @return string The plain text representation of the HTML content.
138     */
139    private function htmlToPlainText(string $html): string
140    {
141        $text = strip_tags($html);
142        $text = html_entity_decode($text);
143
144        return trim($text);
145    }
146
147    /**
148     * Deletes an email template.
149     *
150     * @param string|null $id Email Template id.
151     * @return \Cake\Http\Response Redirects to index.
152     * @throws \Cake\Datasource\Exception\RecordNotFoundException When record not found.
153     */
154    public function delete(?string $id = null): Response
155    {
156        $this->request->allowMethod(['post', 'delete']);
157        $emailTemplate = $this->EmailTemplates->get($id);
158        if ($this->EmailTemplates->delete($emailTemplate)) {
159            $this->Flash->success(__('The email template has been deleted.'));
160        } else {
161            $this->Flash->error(__('The email template could not be deleted. Please, try again.'));
162        }
163
164        return $this->redirect(['action' => 'index']);
165    }
166
167    /**
168     * Sends an email to a user based on a selected email template.
169     *
170     * @return \Cake\Http\Response|null Redirects after attempting to send the email, or renders view.
171     * @throws \Cake\Datasource\Exception\RecordNotFoundException When email template or user not found.
172     * @throws \Exception If there's an error during the email sending process.
173     */
174    public function sendEmail(): ?Response
175    {
176        $emailTemplates = $this->EmailTemplates->find(
177            'list',
178            keyField: 'id',
179            valueField: 'name',
180        );
181
182        $usersTable = TableRegistry::getTableLocator()->get('Users');
183        $users = $usersTable->find(
184            'list',
185            keyField: 'id',
186            valueField: 'email',
187        );
188
189        if ($this->request->is('post')) {
190            $data = $this->request->getData();
191
192            $variables = $this->prepareEmailVariables($data['email_template_id'], $data['user_id']);
193            $emailTemplate = $this->EmailTemplates->get($data['email_template_id']);
194            $user = $usersTable->get($data['user_id']);
195
196            $bodyHtml = $emailTemplate->body_html ?? '';
197            $bodyPlain = $emailTemplate->body_plain ?? '';
198
199            // Replace all placeholders
200            foreach ($variables as $key => $value) {
201                $bodyHtml = str_replace('{' . $key . '}', $value, $bodyHtml);
202                $bodyPlain = str_replace('{' . $key . '}', $value, $bodyPlain);
203            }
204
205            $mailer = new Mailer('default');
206            $mailer->setTo($user->email)
207                ->setSubject($emailTemplate->subject)
208                ->setEmailFormat('both')
209                ->setViewVars([
210                    'bodyHtml' => $bodyHtml,
211                    'bodyPlain' => $bodyPlain,
212                ])
213                ->viewBuilder()
214                    ->setTemplate('default')
215                    ->setLayout('default')
216                    ->setPlugin('AdminTheme');
217
218            try {
219                $result = $mailer->deliver();
220                if ($result) {
221                    $this->Flash->success(__('Email sent successfully.'));
222                    Log::info(
223                        __(
224                            'Email sent successfully to: {0}. Template: {1}, Subject: {2}',
225                            $user->email,
226                            $emailTemplate->template_identifier,
227                            $emailTemplate->subject,
228                        ),
229                        ['group_name' => 'email'],
230                    );
231                } else {
232                    $this->Flash->error(__('Failed to send email. Please check your email configuration.'));
233                    Log::error(
234                        __('Failed to send email to: {0}', $user->email),
235                        ['group_name' => 'email'],
236                    );
237                }
238            } catch (Exception $e) {
239                $this->Flash->error(__('Error sending email: {0}', $e->getMessage()));
240                Log::error(
241                    __('Error sending email: {0}', $e->getMessage()),
242                    ['group_name' => 'email', 'exception' => $e],
243                );
244            }
245
246            return $this->redirect(['action' => 'index']);
247        }
248
249        $this->set(compact('emailTemplates', 'users'));
250
251        return null;
252    }
253
254    /**
255     * Prepares variables for email templates.
256     *
257     * This method generates a set of variables to be used in email templates.
258     * It always includes basic user information and conditionally adds other
259     * variables based on the content of the email template.
260     *
261     * @param string $templateId The UUID of the email template.
262     * @param string $userId The ID of the user for whom the email is being prepared.
263     * @return array An associative array of variables for use in the email template.
264     */
265    private function prepareEmailVariables(string $templateId, string $userId): array
266    {
267        $variables = [];
268        $emailTemplate = $this->EmailTemplates->get($templateId, contain: []);
269        $user = TableRegistry::getTableLocator()->get('Users')->get($userId, contain: []);
270
271        $variables['username'] = $user->username;
272        $variables['email'] = $user->email;
273
274        if (
275            strpos($emailTemplate->body_html, '{confirm_email_link}') !== false ||
276            strpos($emailTemplate->body_plain, '{confirm_email_link}') !== false
277        ) {
278            $variables['confirm_email_link'] = $this->generateLink($user, 'confirm_email_link');
279        }
280
281        if (
282            strpos($emailTemplate->body_html, '{reset_password_link}') !== false ||
283            strpos($emailTemplate->body_plain, '{reset_password_link}') !== false
284        ) {
285            $variables['reset_password_link'] = $this->generateLink($user, 'reset_password_link');
286        }
287
288        return $variables;
289    }
290
291    /**
292     * Generates a confirmation link for a user.
293     *
294     * This method retrieves the confirmation code for a given user from the
295     * UserAccountConfirmations table. If a confirmation code does not exist,
296     * it generates a new UUID as the confirmation code, saves it to the table,
297     * and then generates a URL for the user to confirm their account.
298     *
299     * @param \App\Model\Entity\User $user The user entity for whom the confirmation link is generated.
300     * @return string The generated confirmation link URL.
301     */
302    private function generateLink(User $user, string $emailTemplateId): string
303    {
304        $userAccountConfirmationsTable = TableRegistry::getTableLocator()->get('UserAccountConfirmations');
305        $confirmation = $userAccountConfirmationsTable->find()
306            ->where(['user_id' => $user->id])
307            ->first();
308
309        if ($confirmation) {
310            $confirmationCode = $confirmation->confirmation_code;
311        } else {
312            $confirmationCode = Text::uuid();
313            $newConfirmation = $userAccountConfirmationsTable->newEntity([
314                'user_id' => $user->id,
315                'confirmation_code' => $confirmationCode,
316            ]);
317            $userAccountConfirmationsTable->save($newConfirmation);
318        }
319
320        switch ($emailTemplateId) {
321            case 'reset_password_link':
322                return Router::url([
323                    '_name' => 'reset-password',
324                    $confirmationCode,
325                ], true);
326
327            case 'confirm_email_link':
328                return Router::url([
329                    '_name' => 'confirm-email',
330                    $confirmationCode,
331                ], true);
332
333            default:
334                return '';
335        }
336    }
337}