PluginProbe
The Innovative Form Builder – IvyForms / 1.5.1
The Innovative Form Builder – IvyForms v1.5.1
1.5.1 1.5 1.4.1 1.4 trunk 0.1.2 0.2 0.2.1 0.3 0.3.1 0.4 0.5 0.6 0.6.1 0.6.1-backup 0.6.1.1 0.7 0.8 0.8.1 0.8.2 0.9 0.9.1 1.0 1.1 1.1.1 All 27 releases
ivyforms / backend / src / Services / Notification / NotificationService.php

NotificationService.php in The Innovative Form Builder – IvyForms 1.5.1, at backend/src/Services/Notification/NotificationService.php

627 lines 18.7 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 namespace IvyForms\Services\Notification;
4
5 use IvyForms\Common\Exceptions\InvalidArgumentException;
6 use IvyForms\Common\Exceptions\NotFoundException;
7 use IvyForms\Common\Exceptions\QueryExecutionException;
8 use IvyForms\Common\Sanitizer\HtmlSanitizer;
9 use IvyForms\Common\Sanitizer\Sanitizer;
10 use IvyForms\Entity\Notification\Notification;
11 use IvyForms\Factory\Notification\NotificationFactory;
12 use IvyForms\Repository\Notification\NotificationRepositoryInterface;
13 use IvyForms\Services\Placeholder\PlaceholderService;
14 use IvyForms\Services\Media\ImageService;
15 use IvyForms\Services\Translations\BackendStrings;
16 use IvyForms\Services\Mailer\MailerService;
17
18 /**
19 * @SuppressWarnings(PHPMD.ExcessiveClassComplexity)
20 */
21 class NotificationService
22 {
23 private NotificationRepositoryInterface $notificationRepository;
24 private ImageService $imageService;
25
26 // Constructor injection for the NotificationRepository via PHP-DI
27 public function __construct(
28 NotificationRepositoryInterface $notificationRepository,
29 ImageService $imageService
30 ) {
31 $this->notificationRepository = $notificationRepository;
32 $this->imageService = $imageService;
33 }
34
35 /**
36 * Get all notifications from the repository.
37 *
38 * @return array<mixed>|null
39 */
40 public function getAllNotifications(): ?array
41 {
42 return $this->notificationRepository->getAll();
43 }
44
45 /**
46 * Create a new notification.
47 *
48 * @param Notification $notificationData
49 *
50 * @return int Notification ID
51 */
52 public function createNotification(Notification $notificationData): int
53 {
54 // Process images in message content before saving
55 $message = $notificationData->getMessage();
56 if (!empty($message)) {
57 $processedMessage = $this->imageService->processImagesInContent($message, 'notification');
58 $notificationData->setMessage($processedMessage);
59 }
60
61 // Create notification via repository and return new notification ID
62 return $this->notificationRepository->add($notificationData);
63 }
64
65 /**
66 * Get a specific notification by its ID.
67 *
68 * @param int $notificationId
69 *
70 * @return object NotificationEntity
71 *
72 * @throws NotFoundException If the notification is not found
73 */
74 public function getNotificationById(int $notificationId): object
75 {
76 $notification = $this->notificationRepository->getById($notificationId);
77
78 if (!$notification) {
79 throw new NotFoundException(
80 BackendStrings::getExceptionStrings()['notification_not_found']
81 );
82 }
83
84 return $notification;
85 }
86
87 /**
88 * Update an existing notification.
89 *
90 * @param int $notificationId
91 * @param Notification $notificationData
92 *
93 * @return bool
94 */
95 public function updateNotification(int $notificationId, Notification $notificationData): bool
96 {
97 // Process images in message content before saving
98 $message = $notificationData->getMessage();
99 if (!empty($message)) {
100 $processedMessage = $this->imageService->processImagesInContent($message, 'notification');
101 $notificationData->setMessage($processedMessage);
102 }
103
104 // Validate notification data before updating
105
106 return $this->notificationRepository->update($notificationId, $notificationData);
107 }
108
109 /**
110 * Delete a notification by its ID.
111 *
112 * @param int $notificationId
113 *
114 * @return int
115 */
116 public function deleteNotification(int $notificationId): int
117 {
118 return $this->notificationRepository->delete($notificationId);
119 }
120
121 /**
122 * Delete multiple notifications by their IDs.
123 *
124 * @param array<int> $notificationIds
125 *
126 * @return int Number of deleted notifications
127 */
128 public function deleteNotifications(array $notificationIds): int
129 {
130 return $this->notificationRepository->deleteMany($notificationIds);
131 }
132
133 /**
134 * Get notifications by form ID.
135 *
136 * @param int $formId
137 *
138 * @return array<Notification>
139 */
140 public function getNotificationsByFormId(int $formId): array
141 {
142 return $this->notificationRepository->getAllById($formId);
143 }
144
145 /**
146 * Get active notifications by form ID.
147 *
148 * @param int $formId
149 *
150 * @return array<Notification>
151 *
152 */
153 public function getActiveNotificationsByFormId(int $formId): array
154 {
155 $notification = $this->notificationRepository->getAllActiveById($formId);
156
157 // TODO Implement handling no notifications found, but do not return an error to the user
158 // For now, log the error if WP_DEBUG is enabled
159 if (!$notification && defined('WP_DEBUG') && WP_DEBUG) {
160 error_log(BackendStrings::getExceptionStrings()['active_notif_not_found']);
161 }
162
163 return $notification;
164 }
165
166 /**
167 * Delete notifications by multiple form IDs.
168 *
169 * @param array<int> $formIds
170 *
171 * @return int Number of deleted notifications
172 * @throws InvalidArgumentException If no form IDs are provided
173 */
174 public function deleteNotificationsByFormIds(array $formIds): int
175 {
176 if (empty($formIds)) {
177 throw new InvalidArgumentException(
178 BackendStrings::getExceptionStrings()['no_form_ids_provided']
179 );
180 }
181
182 return $this->notificationRepository->deleteManyByForeignKeyValues($formIds);
183 }
184
185 /**
186 * Duplicate notifications from one form to another.
187 *
188 * @param int $originalFormId
189 * @param int $newFormId
190 *
191 * @return void
192 * @throws NotFoundException
193 * @throws InvalidArgumentException
194 */
195 public function duplicateNotifications(int $originalFormId, int $newFormId): void
196 {
197 $notifications = $this->getNotificationsByFormId($originalFormId);
198 foreach ($notifications as $notification) {
199 $notificationData = $notification->toArray();
200 unset($notificationData['id']);
201 $notificationData['formId'] = $newFormId;
202
203 $this->createNotification(NotificationFactory::create($notificationData));
204 }
205 }
206
207 /**
208 * Search notifications with pagination and sorting
209 *
210 * @param array<string, mixed> $params
211 *
212 * @return array<string, mixed>
213 */
214 public function searchNotifications(array $params): array
215 {
216 return $this->notificationRepository->search($params);
217 }
218
219 /**
220 * Create a default notification for a form.
221 *
222 * @param int $formId
223 * @param string $formName
224 * @param string $adminEmail
225 *
226 * @return void
227 * @throws InvalidArgumentException|QueryExecutionException
228 */
229 public function createDefaultNotificationForForm(int $formId, string $formName, string $adminEmail): void
230 {
231 $defaultNotification = [
232 'formId' => $formId,
233 'name' => BackendStrings::getAllFormsStrings()['admin_notification_email'],
234 'sender' => $adminEmail,
235 'receiver' => $adminEmail,
236 'enabled' => 1,
237 'subject' => $formName,
238 'message' => '{{all_data}}',
239 'smartLogic' => [
240 'enabled' => false,
241 'match' => 'any',
242 'rules' => [],
243 ],
244 ];
245 $notification = NotificationFactory::create($defaultNotification);
246 $notificationId = $this->createNotification($notification);
247 if (!$notificationId) {
248 throw new QueryExecutionException(
249 BackendStrings::getSettingsFormBuilderStrings()['failed_to_create_default_notification']
250 );
251 }
252 $notification->setId($notificationId);
253 }
254
255 /**
256 * Process notifications for the form.
257 *
258 * @param int $formId
259 * @param array<mixed> $submissionData
260 * @param array<mixed> $formFields
261 * @param array<string, mixed> $fieldData
262 * @param array<string, mixed> $generalData
263 * @param MailerService $mailerService
264 * @param array<string, string> $fieldLabels
265 * @return bool
266 */
267 public function processNotifications(
268 int $formId,
269 array $submissionData,
270 array $formFields,
271 array $fieldData,
272 array $generalData,
273 MailerService $mailerService,
274 array $fieldLabels = []
275 ): bool {
276 $activeNotifications = $this->getActiveNotificationsByFormId($formId);
277 $exceptionStrings = BackendStrings::getExceptionStrings();
278
279 foreach ($activeNotifications as $notification) {
280 if (empty($notification->getNotificationContact()->getReceiver())) {
281 continue;
282 }
283
284 $shouldSend = apply_filters(
285 'ivyforms/notification/should_send',
286 true,
287 $notification->getSmartLogic(),
288 $submissionData,
289 $formFields,
290 $notification,
291 $formId
292 );
293
294 if (!$shouldSend) {
295 continue;
296 }
297
298 if (
299 !$this->dispatchNotificationEmail(
300 $notification,
301 $formId,
302 $submissionData,
303 $formFields,
304 $fieldData,
305 $generalData,
306 $mailerService,
307 $fieldLabels,
308 $exceptionStrings
309 )
310 ) {
311 return false;
312 }
313 }
314
315 return true;
316 }
317
318 /**
319 * Resolve placeholders, apply attachments, and send one notification email.
320 *
321 * @param Notification $notification
322 * @param int $formId
323 * @param array<mixed> $submissionData
324 * @param array<mixed> $formFields
325 * @param array<string, mixed> $fieldData
326 * @param array<string, mixed> $generalData
327 * @param MailerService $mailerService
328 * @param array<string, string> $fieldLabels
329 * @param array<string, string> $exceptionStrings
330 * @return bool False when sender/receiver are invalid; true otherwise (send failure is logged)
331 */
332 private function dispatchNotificationEmail(
333 Notification $notification,
334 int $formId,
335 array $submissionData,
336 array $formFields,
337 array $fieldData,
338 array $generalData,
339 MailerService $mailerService,
340 array $fieldLabels,
341 array $exceptionStrings
342 ): bool {
343 if (
344 !$this->hydrateNotificationForSend(
345 $notification,
346 $fieldData,
347 $generalData,
348 $fieldLabels,
349 $exceptionStrings
350 )
351 ) {
352 return false;
353 }
354
355 $this->sendHydratedNotificationEmail(
356 $notification,
357 $formId,
358 $submissionData,
359 $formFields,
360 $fieldData,
361 $mailerService,
362 $exceptionStrings
363 );
364
365 return true;
366 }
367
368 /**
369 * Resolve and apply sender/receiver/replyTo/cc/bcc/subject/message onto the notification.
370 *
371 * @param Notification $notification
372 * @param array<string, mixed> $fieldData
373 * @param array<string, mixed> $generalData
374 * @param array<string, string> $fieldLabels
375 * @param array<string, string> $exceptionStrings
376 */
377 private function hydrateNotificationForSend(
378 Notification $notification,
379 array $fieldData,
380 array $generalData,
381 array $fieldLabels,
382 array $exceptionStrings
383 ): bool {
384 $contact = $notification->getNotificationContact();
385 $sender = $this->resolveAddress($contact->getSender(), $fieldData, $generalData, $fieldLabels);
386 $receiver = $this->resolveAddress($contact->getReceiver(), $fieldData, $generalData, $fieldLabels);
387
388 if ($sender === '' || $receiver === '') {
389 error_log(
390 $exceptionStrings['error_log_invalid_email'] . $sender . ' ' . $receiver
391 );
392 return false;
393 }
394
395 $contact->setSender($sender);
396 $contact->setReceiver($receiver);
397 $contact->setReplyTo(
398 $this->resolveReplyTo($contact->getReplyTo(), $sender, $fieldData, $generalData, $fieldLabels)
399 );
400 $contact->setCc(
401 $this->resolveAddressList($contact->getCc(), $fieldData, $generalData, $fieldLabels)
402 );
403 $contact->setBcc(
404 $this->resolveAddressList($contact->getBcc(), $fieldData, $generalData, $fieldLabels)
405 );
406 $notification->setSubject(
407 PlaceholderService::replacePlaceholders(
408 $notification->getSubject(),
409 $fieldData,
410 $generalData,
411 $fieldLabels,
412 $notification
413 )
414 );
415 $notification->setMessage(
416 $this->buildSafeMessage(
417 $notification->getMessage(),
418 $fieldData,
419 $generalData,
420 $fieldLabels,
421 $notification
422 )
423 );
424
425 return true;
426 }
427
428 /**
429 * @param Notification $notification
430 * @param int $formId
431 * @param array<mixed> $submissionData
432 * @param array<mixed> $formFields
433 * @param array<string, mixed> $fieldData
434 * @param MailerService $mailerService
435 * @param array<string, string> $exceptionStrings
436 */
437 private function sendHydratedNotificationEmail(
438 Notification $notification,
439 int $formId,
440 array $submissionData,
441 array $formFields,
442 array $fieldData,
443 MailerService $mailerService,
444 array $exceptionStrings
445 ): void {
446 $sent = $mailerService->sendEmail(
447 $notification,
448 $submissionData,
449 $this->resolveEmailAttachments($notification, $submissionData, $formFields, $fieldData)
450 );
451
452 if ($sent) {
453 return;
454 }
455
456 error_log($exceptionStrings['error_log_failed_send'] . ' ' . $formId);
457 }
458
459 /**
460 * @param string $template
461 * @param string $sender
462 * @param array<string, mixed> $fieldData
463 * @param array<string, mixed> $generalData
464 * @param array<string, string> $fieldLabels
465 */
466 private function resolveReplyTo(
467 string $template,
468 string $sender,
469 array $fieldData,
470 array $generalData,
471 array $fieldLabels
472 ): string {
473 $replyTo = $this->resolveAddress($template, $fieldData, $generalData, $fieldLabels);
474
475 return $replyTo !== '' ? $replyTo : $sender;
476 }
477
478 /**
479 * @param Notification $notification
480 * @param array<mixed> $submissionData
481 * @param array<mixed> $formFields
482 * @param array<string, mixed> $fieldData
483 * @return string[]
484 */
485 private function resolveEmailAttachments(
486 Notification $notification,
487 array $submissionData,
488 array $formFields,
489 array $fieldData
490 ): array {
491 /**
492 * Local file paths to attach to the notification email (Pro file attachments).
493 *
494 * @since 1.2.0
495 *
496 * @param mixed $attachments
497 * @param \IvyForms\Entity\Notification\Notification $notification
498 * @param array<string, mixed> $submissionData
499 * @param array<int, mixed> $formFields
500 * @param array<string, mixed> $fieldData
501 */
502 $attachments = apply_filters(
503 'ivyforms/notification/email_attachments',
504 [],
505 $notification,
506 $submissionData,
507 $formFields,
508 $fieldData
509 );
510
511 if (!is_array($attachments)) {
512 return [];
513 }
514
515 $paths = [];
516 foreach ($attachments as $path) {
517 if (!is_string($path) || $path === '') {
518 continue;
519 }
520 $paths[] = $path;
521 }
522
523 return $paths;
524 }
525
526 /**
527 * Resolve an email address template that may contain placeholders
528 *
529 * Returns the original replaced string when valid, or empty string when invalid.
530 *
531 * @param string $template
532 * @param array<string,mixed> $fieldData
533 * @param array<string,mixed> $generalData
534 * @param array<string,string> $fieldLabels
535 * @return string
536 */
537 private function resolveAddress(string $template, array $fieldData, array $generalData, array $fieldLabels): string
538 {
539 if (Sanitizer::containsEmailHeaderInjection($template)) {
540 return '';
541 }
542
543 $rawReplaced = PlaceholderService::replacePlaceholders(
544 $template,
545 $fieldData,
546 $generalData,
547 $fieldLabels
548 );
549
550 if (Sanitizer::containsEmailHeaderInjection($rawReplaced)) {
551 return '';
552 }
553
554 $emailCandidate = $rawReplaced;
555 if (preg_match('/<([^>]+)>/', $rawReplaced, $match)) {
556 $emailCandidate = trim($match[1]);
557 }
558
559 return Sanitizer::isValidEmail($emailCandidate) ? $rawReplaced : '';
560 }
561
562 /**
563 * Resolve a comma-separated list of email address templates.
564 *
565 * @param string $template
566 * @param array<string,mixed> $fieldData
567 * @param array<string,mixed> $generalData
568 * @param array<string,string> $fieldLabels
569 * @return string
570 */
571 private function resolveAddressList(
572 string $template,
573 array $fieldData,
574 array $generalData,
575 array $fieldLabels
576 ): string {
577 $template = trim($template);
578 if ($template === '') {
579 return '';
580 }
581
582 $parts = array_map('trim', explode(',', $template));
583 $resolved = [];
584
585 foreach ($parts as $part) {
586 if ($part === '' || Sanitizer::containsEmailHeaderInjection($part)) {
587 continue;
588 }
589
590 $address = $this->resolveAddress($part, $fieldData, $generalData, $fieldLabels);
591 if ($address !== '') {
592 $resolved[] = $address;
593 }
594 }
595
596 return implode(', ', $resolved);
597 }
598
599 /**
600 * Build a sanitized HTML message from a template with placeholders.
601 *
602 * @param string $template
603 * @param array<string,mixed> $fieldData
604 * @param array<string,mixed> $generalData
605 * @param array<string,string> $fieldLabels
606 * @param Notification $notification
607 * @return string
608 */
609 private function buildSafeMessage(
610 string $template,
611 array $fieldData,
612 array $generalData,
613 array $fieldLabels,
614 Notification $notification
615 ): string {
616 $message = PlaceholderService::replacePlaceholders(
617 $template,
618 $fieldData,
619 $generalData,
620 $fieldLabels,
621 $notification
622 );
623
624 return HtmlSanitizer::sanitizeHtmlContent($message);
625 }
626 }
627