PluginProbe
The Innovative Form Builder – IvyForms / 1.5
The Innovative Form Builder – IvyForms v1.5
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/Confirmation/ConfirmationService.php +344 -49 1.1 → 1.5 View file →
@@ -1,30 +1,43 @@
1 1 <?php
2 2
3 3 namespace IvyForms\Services\Confirmation;
4 4
5 +use IvyForms\Common\Exceptions\ForbiddenException;
6 +use IvyForms\Common\Exceptions\InvalidArgumentException;
5 7 use IvyForms\Common\Exceptions\NotFoundException;
6 -use IvyForms\Common\Exceptions\InvalidArgumentException;
7 8 use IvyForms\Common\Exceptions\QueryExecutionException;
9 +use IvyForms\Common\Sanitizer\HtmlSanitizer;
8 10 use IvyForms\Entity\Confirmation\Confirmation;
9 11 use IvyForms\Factory\Confirmation\ConfirmationFactory;
10 12 use IvyForms\Repository\Confirmation\ConfirmationRepositoryInterface;
13 +use IvyForms\Services\Media\ImageService;
11 14 use IvyForms\Services\Placeholder\PlaceholderService;
12 -use IvyForms\Services\Media\ImageService;
13 15 use IvyForms\Services\Translations\BackendStrings;
14 16
17 +/**
18 + * @SuppressWarnings(PHPMD.TooManyPublicMethods)
19 + */
15 20 class ConfirmationService
16 21 {
17 22 private ConfirmationRepositoryInterface $confirmationRepository;
18 23 private ImageService $imageService;
24 + private ConfirmationAccessPolicy $accessPolicy;
25 + private ConfirmationResolver $confirmationResolver;
26 + private ConfirmationOrderService $confirmationOrderService;
19 27
20 - // Constructor injection for the ConfirmationRepository via PHP-DI
21 28 public function __construct(
22 29 ConfirmationRepositoryInterface $confirmationRepository,
23 - ImageService $imageService
30 + ImageService $imageService,
31 + ConfirmationAccessPolicy $accessPolicy,
32 + ConfirmationResolver $confirmationResolver,
33 + ConfirmationOrderService $confirmationOrderService
24 34 ) {
25 35 $this->confirmationRepository = $confirmationRepository;
26 36 $this->imageService = $imageService;
37 + $this->accessPolicy = $accessPolicy;
38 + $this->confirmationResolver = $confirmationResolver;
39 + $this->confirmationOrderService = $confirmationOrderService;
27 40 }
28 41
29 42 /**
30 43 * Create a new confirmation.
@@ -34,16 +47,19 @@
34 47 * @return int Confirmation ID
35 48 */
36 49 public function createConfirmation(Confirmation $confirmationData): int
37 50 {
38 - // Process images in message content before saving
39 - $message = $confirmationData->getMessage();
40 - if (!empty($message)) {
41 - $processedMessage = $this->imageService->processImagesInContent($message, 'confirmation');
42 - $confirmationData->setMessage($processedMessage);
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));
43 58 }
44 59
45 - // Create confirmation via repository and return new confirmation ID
60 + $this->processMessageImages($confirmationData);
61 +
46 62 return $this->confirmationRepository->add($confirmationData);
47 63 }
48 64
49 65 /**
@@ -55,15 +71,20 @@
55 71 * @return bool
56 72 */
57 73 public function updateConfirmation(int $confirmationId, Confirmation $confirmationData): bool
58 74 {
59 - // Process images in message content before saving
60 - $message = $confirmationData->getMessage();
61 - if (!empty($message)) {
62 - $processedMessage = $this->imageService->processImagesInContent($message, 'confirmation');
63 - $confirmationData->setMessage($processedMessage);
64 - }
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);
65 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 +
66 87 return $this->confirmationRepository->update($confirmationId, $confirmationData);
67 88 }
68 89
69 90 /**
@@ -70,17 +91,19 @@
70 91 * Get confirmations by form ID.
71 92 *
72 93 * @param int $id
73 94 *
74 - * @return mixed[]|null
95 + * @return array<Confirmation>
75 96 *
76 97 * @throws NotFoundException If no confirmations are found
77 98 */
78 - public function getConfirmationsById(int $id): ?array
99 + public function getConfirmationsById(int $id): array
79 100 {
101 + $this->ensureDefaultConfirmationExists($id);
102 +
80 103 $confirmation = $this->confirmationRepository->getAllById($id);
81 104
82 - if (!$confirmation) {
105 + if (empty($confirmation)) {
83 106 throw new NotFoundException(
84 107 BackendStrings::getExceptionStrings()['confirmation_not_found']
85 108 );
86 109 }
@@ -86,11 +109,26 @@
86 109 }
87 110
88 111 return $confirmation;
89 112 }
113 +
90 114 /**
91 - * Get a specific form by its ID.
115 + * Ensure a form has at least one default confirmation.
92 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 + *
93 131 * @param int $id
94 132 *
95 133 * @return object ConfirmationEntity
96 134 *
@@ -109,8 +147,41 @@
109 147 return $confirmation;
110 148 }
111 149
112 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 + /**
113 184 * Delete confirmations by multiple form IDs.
114 185 *
115 186 * @param array<int> $formIds
116 187 * @return int Number of deleted confirmations
@@ -127,8 +198,89 @@
127 198 return $this->confirmationRepository->deleteManyByForeignKeyValues($formIds);
128 199 }
129 200
130 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 + /**
131 283 * Duplicate confirmations from one form to another.
132 284 *
133 285 * @param int $originalFormId
134 286 * @param int $newFormId
@@ -138,14 +290,18 @@
138 290 */
139 291 public function duplicateConfirmations(int $originalFormId, int $newFormId): void
140 292 {
141 293 $confirmations = $this->getConfirmationsById($originalFormId);
142 - foreach ($confirmations as $confirmation) {
294 + foreach ($confirmations as $index => $confirmation) {
143 295 $confirmationData = $confirmation->toArray();
144 - unset($confirmationData['id']);
296 + unset($confirmationData['id'], $confirmationData['pageUrl']);
145 297 $confirmationData['formId'] = $newFormId;
298 + $confirmationData['position'] = $index;
299 + $confirmationData['isDefault'] = $index === 0;
146 300
147 - $this->createConfirmation(ConfirmationFactory::create($confirmationData));
301 + $entity = ConfirmationFactory::create($confirmationData);
302 + $this->processMessageImages($entity);
303 + $this->confirmationRepository->add($entity);
148 304 }
149 305 }
150 306
151 307 /**
@@ -155,19 +311,28 @@
155 311 * @return int Confirmation ID
156 312 */
157 313 public function createDefaultConfirmationForForm(int $formId): int
158 314 {
315 + $strings = BackendStrings::getSettingsFormBuilderStrings();
159 316 $defaultConfirmation = [
160 - 'formId' => $formId,
161 - 'type' => 'successMessage',
162 - 'enabled' => 1,
163 - 'showForm' => 0,
164 - 'message' => BackendStrings::getAllFormsStrings()['thanks_reaching'],
165 - 'url' => '',
166 - 'page' => '',
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 + ],
167 332 ];
168 333 $confirmation = ConfirmationFactory::create($defaultConfirmation);
169 - $confirmationId = $this->createConfirmation($confirmation);
334 + $confirmationId = $this->confirmationRepository->add($confirmation);
170 335 if (!$confirmationId) {
171 336 throw new QueryExecutionException(
172 337 BackendStrings::getSettingsFormBuilderStrings()['failed_to_create_confirmation']
173 338 );
@@ -176,38 +341,168 @@
176 341 return $confirmationId;
177 342 }
178 343
179 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 + /**
180 425 * Process confirmations for the form.
181 426 *
182 427 * @param int $formId
428 + * @param array<string, mixed> $submissionData
429 + * @param array<int, mixed> $formFields
183 430 * @param array<string, mixed> $fieldData
184 431 * @param array<string, mixed> $generalData
185 432 * @param array<string, string> $fieldLabels
186 - * @return string
433 + * @return array<string, mixed>
187 434 * @throws NotFoundException
188 435 */
189 436 public function processConfirmations(
190 437 int $formId,
438 + array $submissionData,
439 + array $formFields,
191 440 array $fieldData,
192 441 array $generalData,
193 442 array $fieldLabels = []
194 - ): string {
195 - $confirmations = $this->getConfirmationsById($formId);
196 - foreach ($confirmations as $confirmation) {
197 - if (
198 - !empty($confirmation) &&
199 - $confirmation->isEnabled() &&
200 - $confirmation->getType() === 'successMessage'
201 - ) {
202 - $confirmationMessage = $confirmation->getMessage();
203 - return PlaceholderService::replacePlaceholders(
204 - $confirmationMessage,
205 - $fieldData,
206 - $generalData,
207 - $fieldLabels
208 - );
209 - }
443 + ): array {
444 + $confirmation = $this->resolveActiveConfirmation($formId, $submissionData, $formFields);
445 + if ($confirmation === null) {
446 + $confirmation = $this->getActiveConfirmation($formId);
210 447 }
211 - return '';
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 + );
212 507 }
213 508 }