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
← All changes | backend/src/Services/Notification/NotificationService.php +626 -0 0.6.1 → 1.5.1 View file →
@@ -1,0 +1,626 @@
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 +}