PluginProbe
The Innovative Form Builder – IvyForms / 1.4.1
The Innovative Form Builder – IvyForms v1.4.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.4.1, at backend/src/Services/Notification/NotificationService.php

489 lines 15.0 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 class NotificationService
19 {
20 private NotificationRepositoryInterface $notificationRepository;
21 private ImageService $imageService;
22
23 // Constructor injection for the NotificationRepository via PHP-DI
24 public function __construct(
25 NotificationRepositoryInterface $notificationRepository,
26 ImageService $imageService
27 ) {
28 $this->notificationRepository = $notificationRepository;
29 $this->imageService = $imageService;
30 }
31
32 /**
33 * Get all notifications from the repository.
34 *
35 * @return array<mixed>|null
36 */
37 public function getAllNotifications(): ?array
38 {
39 return $this->notificationRepository->getAll();
40 }
41
42 /**
43 * Create a new notification.
44 *
45 * @param Notification $notificationData
46 *
47 * @return int Notification ID
48 */
49 public function createNotification(Notification $notificationData): int
50 {
51 // Process images in message content before saving
52 $message = $notificationData->getMessage();
53 if (!empty($message)) {
54 $processedMessage = $this->imageService->processImagesInContent($message, 'notification');
55 $notificationData->setMessage($processedMessage);
56 }
57
58 // Create notification via repository and return new notification ID
59 return $this->notificationRepository->add($notificationData);
60 }
61
62 /**
63 * Get a specific notification by its ID.
64 *
65 * @param int $notificationId
66 *
67 * @return object NotificationEntity
68 *
69 * @throws NotFoundException If the notification is not found
70 */
71 public function getNotificationById(int $notificationId): object
72 {
73 $notification = $this->notificationRepository->getById($notificationId);
74
75 if (!$notification) {
76 throw new NotFoundException(
77 BackendStrings::getExceptionStrings()['notification_not_found']
78 );
79 }
80
81 return $notification;
82 }
83
84 /**
85 * Update an existing notification.
86 *
87 * @param int $notificationId
88 * @param Notification $notificationData
89 *
90 * @return bool
91 */
92 public function updateNotification(int $notificationId, Notification $notificationData): bool
93 {
94 // Process images in message content before saving
95 $message = $notificationData->getMessage();
96 if (!empty($message)) {
97 $processedMessage = $this->imageService->processImagesInContent($message, 'notification');
98 $notificationData->setMessage($processedMessage);
99 }
100
101 // Validate notification data before updating
102
103 return $this->notificationRepository->update($notificationId, $notificationData);
104 }
105
106 /**
107 * Delete a notification by its ID.
108 *
109 * @param int $notificationId
110 *
111 * @return int
112 */
113 public function deleteNotification(int $notificationId): int
114 {
115 return $this->notificationRepository->delete($notificationId);
116 }
117
118 /**
119 * Delete multiple notifications by their IDs.
120 *
121 * @param array<int> $notificationIds
122 *
123 * @return int Number of deleted notifications
124 */
125 public function deleteNotifications(array $notificationIds): int
126 {
127 return $this->notificationRepository->deleteMany($notificationIds);
128 }
129
130 /**
131 * Get notifications by form ID.
132 *
133 * @param int $formId
134 *
135 * @return array<Notification>
136 *
137 * @throws NotFoundException If no notifications are found
138 */
139 public function getNotificationsByFormId(int $formId): array
140 {
141 $notification = $this->notificationRepository->getAllById($formId);
142
143 if (!$notification) {
144 throw new NotFoundException(
145 BackendStrings::getExceptionStrings()['notification_not_found']
146 );
147 }
148
149 return $notification;
150 }
151
152 /**
153 * Get active notifications by form ID.
154 *
155 * @param int $formId
156 *
157 * @return array<Notification>
158 *
159 */
160 public function getActiveNotificationsByFormId(int $formId): array
161 {
162 $notification = $this->notificationRepository->getAllActiveById($formId);
163
164 // TODO Implement handling no notifications found, but do not return an error to the user
165 // For now, log the error if WP_DEBUG is enabled
166 if (!$notification && defined('WP_DEBUG') && WP_DEBUG) {
167 error_log(BackendStrings::getExceptionStrings()['active_notif_not_found']);
168 }
169
170 return $notification;
171 }
172
173 /**
174 * Delete notifications by multiple form IDs.
175 *
176 * @param array<int> $formIds
177 *
178 * @return int Number of deleted notifications
179 * @throws InvalidArgumentException If no form IDs are provided
180 */
181 public function deleteNotificationsByFormIds(array $formIds): int
182 {
183 if (empty($formIds)) {
184 throw new InvalidArgumentException(
185 BackendStrings::getExceptionStrings()['no_form_ids_provided']
186 );
187 }
188
189 return $this->notificationRepository->deleteManyByForeignKeyValues($formIds);
190 }
191
192 /**
193 * Duplicate notifications from one form to another.
194 *
195 * @param int $originalFormId
196 * @param int $newFormId
197 *
198 * @return void
199 * @throws NotFoundException
200 * @throws InvalidArgumentException
201 */
202 public function duplicateNotifications(int $originalFormId, int $newFormId): void
203 {
204 $notifications = $this->getNotificationsByFormId($originalFormId);
205 foreach ($notifications as $notification) {
206 $notificationData = $notification->toArray();
207 unset($notificationData['id']);
208 $notificationData['formId'] = $newFormId;
209
210 $this->createNotification(NotificationFactory::create($notificationData));
211 }
212 }
213
214 /**
215 * Search notifications with pagination and sorting
216 *
217 * @param array<string, mixed> $params
218 *
219 * @return array<string, mixed>
220 */
221 public function searchNotifications(array $params): array
222 {
223 return $this->notificationRepository->search($params);
224 }
225
226 /**
227 * Create a default notification for a form.
228 *
229 * @param int $formId
230 * @param string $formName
231 * @param string $adminEmail
232 *
233 * @return void
234 * @throws InvalidArgumentException|QueryExecutionException
235 */
236 public function createDefaultNotificationForForm(int $formId, string $formName, string $adminEmail): void
237 {
238 $defaultNotification = [
239 'formId' => $formId,
240 'name' => BackendStrings::getAllFormsStrings()['admin_notification_email'],
241 'sender' => $adminEmail,
242 'receiver' => $adminEmail,
243 'enabled' => 1,
244 'subject' => $formName,
245 'message' => '{{all_data}}',
246 'smartLogic' => [
247 'enabled' => false,
248 'match' => 'any',
249 'rules' => [],
250 ],
251 ];
252 $notification = NotificationFactory::create($defaultNotification);
253 $notificationId = $this->createNotification($notification);
254 if (!$notificationId) {
255 throw new QueryExecutionException(
256 BackendStrings::getSettingsFormBuilderStrings()['failed_to_create_default_notification']
257 );
258 }
259 $notification->setId($notificationId);
260 }
261
262 /**
263 * Process notifications for the form.
264 *
265 * @param int $formId
266 * @param array<mixed> $submissionData
267 * @param array<mixed> $formFields
268 * @param array<string, mixed> $fieldData
269 * @param array<string, mixed> $generalData
270 * @param MailerService $mailerService
271 * @param array<string, string> $fieldLabels
272 * @return bool
273 */
274 public function processNotifications(
275 int $formId,
276 array $submissionData,
277 array $formFields,
278 array $fieldData,
279 array $generalData,
280 MailerService $mailerService,
281 array $fieldLabels = []
282 ): bool {
283 $activeNotifications = $this->getActiveNotificationsByFormId($formId);
284 $logicFormFields = $formFields;
285 $exceptionStrings = BackendStrings::getExceptionStrings();
286 foreach ($activeNotifications as $notification) {
287 if (empty($notification->getNotificationContact()->getReceiver())) {
288 continue;
289 }
290
291 $shouldSend = apply_filters(
292 'ivyforms/notification/should_send',
293 true,
294 $notification->getSmartLogic(),
295 $submissionData,
296 $logicFormFields,
297 $notification,
298 $formId
299 );
300
301 if (!$shouldSend) {
302 continue;
303 }
304
305 // Resolve sender and receiver
306 $sender = $this->resolveAddress(
307 $notification->getNotificationContact()->getSender(),
308 $fieldData,
309 $generalData,
310 $fieldLabels
311 );
312
313 $receiver = $this->resolveAddress(
314 $notification->getNotificationContact()->getReceiver(),
315 $fieldData,
316 $generalData,
317 $fieldLabels
318 );
319
320 $cc = $this->resolveAddressList(
321 $notification->getNotificationContact()->getCc(),
322 $fieldData,
323 $generalData,
324 $fieldLabels
325 );
326
327 $bcc = $this->resolveAddressList(
328 $notification->getNotificationContact()->getBcc(),
329 $fieldData,
330 $generalData,
331 $fieldLabels
332 );
333
334 // Build subject and sanitized message
335 $subject = PlaceholderService::replacePlaceholders(
336 $notification->getSubject(),
337 $fieldData,
338 $generalData,
339 $fieldLabels,
340 $notification
341 );
342
343 $safeMessage = $this->buildSafeMessage(
344 $notification->getMessage(),
345 $fieldData,
346 $generalData,
347 $fieldLabels,
348 $notification
349 );
350
351 // Resolve replyTo and validate; fallback to sender if empty
352 $replyTo = $this->resolveAddress(
353 $notification->getNotificationContact()->getReplyTo(),
354 $fieldData,
355 $generalData,
356 $fieldLabels
357 );
358 if (empty($replyTo) && !empty($sender)) {
359 $replyTo = $sender;
360 }
361
362 if (empty($sender) || empty($receiver)) {
363 error_log(
364 $exceptionStrings['error_log_invalid_email'] . $sender . ' ' . $receiver
365 );
366 return false;
367 }
368
369 // Apply resolved values back to the notification
370 $notification->getNotificationContact()->setSender($sender);
371 $notification->getNotificationContact()->setReceiver($receiver);
372 $notification->getNotificationContact()->setReplyTo($replyTo);
373 $notification->getNotificationContact()->setCc($cc);
374 $notification->getNotificationContact()->setBcc($bcc);
375 $notification->setSubject($subject);
376 $notification->setMessage($safeMessage);
377
378 // Send notification
379 $sent = $mailerService->sendEmail($notification, $submissionData);
380 if (!$sent) {
381 error_log($exceptionStrings['error_log_failed_send'] . ' ' . $formId);
382 }
383 }
384
385 return true;
386 }
387
388 /**
389 * Resolve an email address template that may contain placeholders
390 *
391 * Returns the original replaced string when valid, or empty string when invalid.
392 *
393 * @param string $template
394 * @param array<string,mixed> $fieldData
395 * @param array<string,mixed> $generalData
396 * @param array<string,string> $fieldLabels
397 * @return string
398 */
399 private function resolveAddress(string $template, array $fieldData, array $generalData, array $fieldLabels): string
400 {
401 if (Sanitizer::containsEmailHeaderInjection($template)) {
402 return '';
403 }
404
405 $rawReplaced = PlaceholderService::replacePlaceholders(
406 $template,
407 $fieldData,
408 $generalData,
409 $fieldLabels
410 );
411
412 if (Sanitizer::containsEmailHeaderInjection($rawReplaced)) {
413 return '';
414 }
415
416 $emailCandidate = $rawReplaced;
417 if (preg_match('/<([^>]+)>/', $rawReplaced, $match)) {
418 $emailCandidate = trim($match[1]);
419 }
420
421 return Sanitizer::isValidEmail($emailCandidate) ? $rawReplaced : '';
422 }
423
424 /**
425 * Resolve a comma-separated list of email address templates.
426 *
427 * @param string $template
428 * @param array<string,mixed> $fieldData
429 * @param array<string,mixed> $generalData
430 * @param array<string,string> $fieldLabels
431 * @return string
432 */
433 private function resolveAddressList(
434 string $template,
435 array $fieldData,
436 array $generalData,
437 array $fieldLabels
438 ): string {
439 $template = trim($template);
440 if ($template === '') {
441 return '';
442 }
443
444 $parts = array_map('trim', explode(',', $template));
445 $resolved = [];
446
447 foreach ($parts as $part) {
448 if ($part === '' || Sanitizer::containsEmailHeaderInjection($part)) {
449 continue;
450 }
451
452 $address = $this->resolveAddress($part, $fieldData, $generalData, $fieldLabels);
453 if ($address !== '') {
454 $resolved[] = $address;
455 }
456 }
457
458 return implode(', ', $resolved);
459 }
460
461 /**
462 * Build a sanitized HTML message from a template with placeholders.
463 *
464 * @param string $template
465 * @param array<string,mixed> $fieldData
466 * @param array<string,mixed> $generalData
467 * @param array<string,string> $fieldLabels
468 * @param Notification $notification
469 * @return string
470 */
471 private function buildSafeMessage(
472 string $template,
473 array $fieldData,
474 array $generalData,
475 array $fieldLabels,
476 Notification $notification
477 ): string {
478 $message = PlaceholderService::replacePlaceholders(
479 $template,
480 $fieldData,
481 $generalData,
482 $fieldLabels,
483 $notification
484 );
485
486 return HtmlSanitizer::sanitizeHtmlContent($message);
487 }
488 }
489