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 / Confirmation / ConfirmationService.php

ConfirmationService.php in The Innovative Form Builder – IvyForms 1.4.1, at backend/src/Services/Confirmation/ConfirmationService.php

509 lines 16.4 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\Confirmation;
4
5 use IvyForms\Common\Exceptions\ForbiddenException;
6 use IvyForms\Common\Exceptions\InvalidArgumentException;
7 use IvyForms\Common\Exceptions\NotFoundException;
8 use IvyForms\Common\Exceptions\QueryExecutionException;
9 use IvyForms\Common\Sanitizer\HtmlSanitizer;
10 use IvyForms\Entity\Confirmation\Confirmation;
11 use IvyForms\Factory\Confirmation\ConfirmationFactory;
12 use IvyForms\Repository\Confirmation\ConfirmationRepositoryInterface;
13 use IvyForms\Services\Media\ImageService;
14 use IvyForms\Services\Placeholder\PlaceholderService;
15 use IvyForms\Services\Translations\BackendStrings;
16
17 /**
18 * @SuppressWarnings(PHPMD.TooManyPublicMethods)
19 */
20 class ConfirmationService
21 {
22 private ConfirmationRepositoryInterface $confirmationRepository;
23 private ImageService $imageService;
24 private ConfirmationAccessPolicy $accessPolicy;
25 private ConfirmationResolver $confirmationResolver;
26 private ConfirmationOrderService $confirmationOrderService;
27
28 public function __construct(
29 ConfirmationRepositoryInterface $confirmationRepository,
30 ImageService $imageService,
31 ConfirmationAccessPolicy $accessPolicy,
32 ConfirmationResolver $confirmationResolver,
33 ConfirmationOrderService $confirmationOrderService
34 ) {
35 $this->confirmationRepository = $confirmationRepository;
36 $this->imageService = $imageService;
37 $this->accessPolicy = $accessPolicy;
38 $this->confirmationResolver = $confirmationResolver;
39 $this->confirmationOrderService = $confirmationOrderService;
40 }
41
42 /**
43 * Create a new confirmation.
44 *
45 * @param Confirmation $confirmationData
46 *
47 * @return int Confirmation ID
48 */
49 public function createConfirmation(Confirmation $confirmationData): int
50 {
51 $formId = $confirmationData->getFormId();
52 $this->accessPolicy->assertCanAdd($formId);
53
54 if ($confirmationData->getPosition() === 0 && $this->confirmationRepository->countByFormId($formId) === 0) {
55 $confirmationData->setIsDefault(true);
56 } elseif ($confirmationData->getPosition() === 0) {
57 $confirmationData->setPosition($this->confirmationRepository->getNextPosition($formId));
58 }
59
60 $this->processMessageImages($confirmationData);
61
62 return $this->confirmationRepository->add($confirmationData);
63 }
64
65 /**
66 * Update an existing confirmation.
67 *
68 * @param int $confirmationId
69 * @param Confirmation $confirmationData
70 *
71 * @return bool
72 */
73 public function updateConfirmation(int $confirmationId, Confirmation $confirmationData): bool
74 {
75 $existing = $this->getConfirmationById($confirmationId);
76 // Always authorize and persist against the stored owner — never trust client formId.
77 $formId = $existing->getFormId();
78 $confirmationData->setFormId($formId);
79
80 $this->accessPolicy->assertCanManage($confirmationId, $formId, 'edit');
81
82 $confirmationData->setPosition($existing->getPosition());
83 $confirmationData->setIsDefault($existing->isDefault());
84
85 $this->processMessageImages($confirmationData);
86
87 return $this->confirmationRepository->update($confirmationId, $confirmationData);
88 }
89
90 /**
91 * Get confirmations by form ID.
92 *
93 * @param int $id
94 *
95 * @return array<Confirmation>
96 *
97 * @throws NotFoundException If no confirmations are found
98 */
99 public function getConfirmationsById(int $id): array
100 {
101 $this->ensureDefaultConfirmationExists($id);
102
103 $confirmation = $this->confirmationRepository->getAllById($id);
104
105 if (empty($confirmation)) {
106 throw new NotFoundException(
107 BackendStrings::getExceptionStrings()['confirmation_not_found']
108 );
109 }
110
111 return $confirmation;
112 }
113
114 /**
115 * Ensure a form has at least one default confirmation.
116 *
117 * @param int $formId
118 * @return int Default confirmation ID
119 */
120 public function ensureDefaultConfirmationExists(int $formId): int
121 {
122 return $this->accessPolicy->ensureDefaultExists(
123 $formId,
124 fn (int $id): int => $this->createDefaultConfirmationForForm($id)
125 );
126 }
127
128 /**
129 * Get a specific confirmation by its ID.
130 *
131 * @param int $id
132 *
133 * @return object ConfirmationEntity
134 *
135 * @throws NotFoundException If the confirmation is not found
136 */
137 public function getConfirmationById(int $id): object
138 {
139 $confirmation = $this->confirmationRepository->getById($id);
140
141 if (!$confirmation) {
142 throw new NotFoundException(
143 BackendStrings::getExceptionStrings()['confirmation_not_found']
144 );
145 }
146
147 return $confirmation;
148 }
149
150 /**
151 * Delete a single confirmation.
152 *
153 * @param int $confirmationId
154 *
155 * @return void
156 * @throws InvalidArgumentException
157 * @throws NotFoundException
158 */
159 public function deleteConfirmation(int $confirmationId): void
160 {
161 $confirmation = $this->getConfirmationById($confirmationId);
162 $formId = $confirmation->getFormId();
163
164 $this->accessPolicy->assertCanManage($confirmationId, $formId, 'delete');
165
166 $count = $this->confirmationRepository->countByFormId($formId);
167
168 if ($count <= 1) {
169 throw new InvalidArgumentException(
170 BackendStrings::getSettingsFormBuilderStrings()['cannot_delete_last_confirmation']
171 );
172 }
173
174 $this->confirmationRepository->delete($confirmationId);
175
176 $remaining = $this->confirmationRepository->getAllById($formId);
177 if (!empty($remaining)) {
178 $orderedIds = array_map(static fn ($item) => $item->getId(), $remaining);
179 $this->confirmationOrderService->applyOrder($formId, $orderedIds);
180 }
181 }
182
183 /**
184 * Delete confirmations by multiple form IDs.
185 *
186 * @param array<int> $formIds
187 * @return int Number of deleted confirmations
188 * @throws InvalidArgumentException If no form IDs are provided
189 */
190 public function deleteConfirmationsByFormIds(array $formIds): int
191 {
192 if (empty($formIds)) {
193 throw new InvalidArgumentException(
194 BackendStrings::getExceptionStrings()['no_form_ids_provided']
195 );
196 }
197
198 return $this->confirmationRepository->deleteManyByForeignKeyValues($formIds);
199 }
200
201 /**
202 * Duplicate a single confirmation.
203 *
204 * @param int $confirmationId
205 *
206 * @return int New confirmation ID
207 * @throws ForbiddenException
208 * @throws NotFoundException
209 * @throws QueryExecutionException
210 */
211 public function duplicateConfirmation(int $confirmationId): int
212 {
213 $original = $this->getConfirmationById($confirmationId);
214 $formId = $original->getFormId();
215
216 $this->accessPolicy->assertCanManage($confirmationId, $formId, 'duplicate');
217 $this->accessPolicy->assertCanAdd($formId);
218
219 $newData = $original->toArray();
220 unset($newData['id'], $newData['pageUrl']);
221 $copyLabel = BackendStrings::getNewFormStrings()['copy_label'];
222 $newData['name'] = $newData['name'] . $copyLabel;
223 $newData['position'] = $this->confirmationRepository->getNextPosition($formId);
224 $newData['isDefault'] = false;
225 $newData['enabled'] = false;
226
227 $newConfirmation = ConfirmationFactory::create($newData);
228 $newId = $this->createConfirmation($newConfirmation);
229
230 if (!$newId) {
231 throw new QueryExecutionException(
232 BackendStrings::getSettingsFormBuilderStrings()['failed_to_duplicate_confirmation']
233 );
234 }
235
236 $newConfirmation->setId($newId);
237 return $newId;
238 }
239
240 /**
241 * Search confirmations with pagination and sorting.
242 *
243 * @param array<string, mixed> $params
244 *
245 * @return array<string, mixed>
246 */
247 public function searchConfirmations(array $params): array
248 {
249 $result = $this->confirmationRepository->search($params);
250
251 $formId = isset($params['filters']['formId']) ? (int) $params['filters']['formId'] : 0;
252 if ($formId > 0 && ($result['meta']['total'] ?? 0) === 0) {
253 $this->ensureDefaultConfirmationExists($formId);
254
255 return $this->confirmationRepository->search($params);
256 }
257
258 return $result;
259 }
260
261 /**
262 * Reorder confirmations for a form.
263 *
264 * @param int $formId
265 * @param array<int, int> $orderedIds
266 *
267 * @return void
268 * @throws ForbiddenException
269 */
270 public function reorderConfirmations(int $formId, array $orderedIds): void
271 {
272 if (!apply_filters('ivyforms/can_create_multiple_confirmations', false, $formId)) {
273 throw new ForbiddenException(
274 BackendStrings::getSettingsFormBuilderStrings()['multiple_confirmations_pro_required']
275 );
276 }
277
278 $this->confirmationOrderService->validateReorderIds($formId, $orderedIds);
279 $this->confirmationOrderService->applyOrder($formId, $orderedIds);
280 }
281
282 /**
283 * Duplicate confirmations from one form to another.
284 *
285 * @param int $originalFormId
286 * @param int $newFormId
287 *
288 * @return void
289 * @throws NotFoundException
290 */
291 public function duplicateConfirmations(int $originalFormId, int $newFormId): void
292 {
293 $confirmations = $this->getConfirmationsById($originalFormId);
294 foreach ($confirmations as $index => $confirmation) {
295 $confirmationData = $confirmation->toArray();
296 unset($confirmationData['id'], $confirmationData['pageUrl']);
297 $confirmationData['formId'] = $newFormId;
298 $confirmationData['position'] = $index;
299 $confirmationData['isDefault'] = $index === 0;
300
301 $entity = ConfirmationFactory::create($confirmationData);
302 $this->processMessageImages($entity);
303 $this->confirmationRepository->add($entity);
304 }
305 }
306
307 /**
308 * Create a default confirmation for a form.
309 *
310 * @param int $formId
311 * @return int Confirmation ID
312 */
313 public function createDefaultConfirmationForForm(int $formId): int
314 {
315 $strings = BackendStrings::getSettingsFormBuilderStrings();
316 $defaultConfirmation = [
317 'formId' => $formId,
318 'name' => $strings['default_confirmation_name'],
319 'type' => 'successMessage',
320 'enabled' => 1,
321 'showForm' => 0,
322 'message' => BackendStrings::getAllFormsStrings()['thanks_reaching'],
323 'url' => '',
324 'page' => '',
325 'position' => 0,
326 'isDefault' => 1,
327 'smartLogic' => [
328 'enabled' => false,
329 'match' => 'any',
330 'rules' => [],
331 ],
332 ];
333 $confirmation = ConfirmationFactory::create($defaultConfirmation);
334 $confirmationId = $this->confirmationRepository->add($confirmation);
335 if (!$confirmationId) {
336 throw new QueryExecutionException(
337 BackendStrings::getSettingsFormBuilderStrings()['failed_to_create_confirmation']
338 );
339 }
340 $confirmation->setId($confirmationId);
341 return $confirmationId;
342 }
343
344 /**
345 * Resolve the first active confirmation for a form (by list order).
346 * Used when submission data is unavailable (e.g. builder preview).
347 *
348 * @param int $formId
349 * @return Confirmation|null
350 * @throws NotFoundException
351 */
352 public function getActiveConfirmation(int $formId): ?Confirmation
353 {
354 $confirmations = $this->getConfirmationsById($formId);
355 foreach ($confirmations as $confirmation) {
356 if ($confirmation->isEnabled()) {
357 return $confirmation;
358 }
359 }
360
361 return null;
362 }
363
364 /**
365 * Resolve which confirmation applies after submission (conditional logic + default fallback).
366 *
367 * @param int $formId
368 * @param array<string, mixed> $submissionData
369 * @param array<int, mixed> $formFields
370 * @return Confirmation|null
371 * @throws NotFoundException
372 */
373 public function resolveActiveConfirmation(
374 int $formId,
375 array $submissionData,
376 array $formFields
377 ): ?Confirmation {
378 return $this->confirmationResolver->resolve(
379 $this->getConfirmationsById($formId),
380 $submissionData,
381 $formFields,
382 $formId
383 );
384 }
385
386 /**
387 * Build the public confirmation payload for a resolved confirmation.
388 *
389 * @param Confirmation $confirmation
390 * @param array<string, mixed> $fieldData
391 * @param array<string, mixed> $generalData
392 * @param array<string, string> $fieldLabels
393 * @return array<string, mixed>
394 */
395 public function buildConfirmationResult(
396 Confirmation $confirmation,
397 array $fieldData,
398 array $generalData,
399 array $fieldLabels = []
400 ): array {
401 $type = $confirmation->getType();
402 $message = '';
403
404 if ($type === 'successMessage') {
405 $message = PlaceholderService::replacePlaceholders(
406 $confirmation->getMessage(),
407 $fieldData,
408 $generalData,
409 $fieldLabels
410 );
411 }
412
413 return [
414 'id' => $confirmation->getId(),
415 'type' => $type,
416 'message' => $message,
417 'showForm' => $confirmation->isShowForm(),
418 'url' => $confirmation->getUrl(),
419 'pageUrl' => $confirmation->getPageUrl(),
420 'isDefault' => $confirmation->isDefault(),
421 ];
422 }
423
424 /**
425 * Process confirmations for the form.
426 *
427 * @param int $formId
428 * @param array<string, mixed> $submissionData
429 * @param array<int, mixed> $formFields
430 * @param array<string, mixed> $fieldData
431 * @param array<string, mixed> $generalData
432 * @param array<string, string> $fieldLabels
433 * @return array<string, mixed>
434 * @throws NotFoundException
435 */
436 public function processConfirmations(
437 int $formId,
438 array $submissionData,
439 array $formFields,
440 array $fieldData,
441 array $generalData,
442 array $fieldLabels = []
443 ): array {
444 $confirmation = $this->resolveActiveConfirmation($formId, $submissionData, $formFields);
445 if ($confirmation === null) {
446 $confirmation = $this->getActiveConfirmation($formId);
447 }
448 if ($confirmation === null) {
449 return [
450 'message' => '',
451 'result' => null,
452 ];
453 }
454
455 $result = $this->buildConfirmationResult($confirmation, $fieldData, $generalData, $fieldLabels);
456
457 return [
458 'message' => $result['message'] ?? '',
459 'result' => $result,
460 ];
461 }
462
463 /**
464 * Sanitize confirmation message and redirect URLs for the submission API response.
465 *
466 * @param array<string, mixed> $confirmationPayload
467 * @return array{message: string, result: array<string, mixed>|null}
468 */
469 public function sanitizeProcessedPayload(array $confirmationPayload): array
470 {
471 $safeConfirmationMessage = HtmlSanitizer::sanitizeEditorContent(
472 $confirmationPayload['message'] ?? ''
473 );
474 $confirmationResult = $confirmationPayload['result'] ?? null;
475
476 if (!is_array($confirmationResult) || !isset($confirmationResult['message'])) {
477 return [
478 'message' => $safeConfirmationMessage,
479 'result' => is_array($confirmationResult) ? $confirmationResult : null,
480 ];
481 }
482
483 $confirmationResult['message'] = $safeConfirmationMessage;
484 if (!empty($confirmationResult['url'])) {
485 $confirmationResult['url'] = esc_url_raw((string) $confirmationResult['url']);
486 }
487 if (!empty($confirmationResult['pageUrl'])) {
488 $confirmationResult['pageUrl'] = esc_url_raw((string) $confirmationResult['pageUrl']);
489 }
490
491 return [
492 'message' => $safeConfirmationMessage,
493 'result' => $confirmationResult,
494 ];
495 }
496
497 private function processMessageImages(Confirmation $confirmationData): void
498 {
499 $message = $confirmationData->getMessage();
500 if ($message === '') {
501 return;
502 }
503
504 $confirmationData->setMessage(
505 $this->imageService->processImagesInContent($message, 'confirmation')
506 );
507 }
508 }
509