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
ivyforms / backend / src / Services / Submission / SubmissionUploadFilePersister.php

SubmissionUploadFilePersister.php in The Innovative Form Builder – IvyForms 1.5, at backend/src/Services/Submission/SubmissionUploadFilePersister.php

188 lines 6.0 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 /**
4 * @copyright © Melograno Venture Studio. All rights reserved.
5 * @licence See COPYING.md for license details.
6 */
7
8 declare(strict_types=1);
9
10 namespace IvyForms\Services\Submission;
11
12 use IvyForms\Common\Exceptions\FieldValidationException;
13 use IvyForms\Entity\Field\Field as FieldEntity;
14 use IvyForms\Infrastructure\WP\REST\SubmissionMultipartReader;
15 use IvyForms\Services\Translations\BackendStrings;
16
17 /**
18 * Validates and persists a single multipart file upload for form submission.
19 */
20 final class SubmissionUploadFilePersister
21 {
22 /**
23 * @param array<string, mixed> $filesLookup
24 * @param array<string, string> $decodedMap
25 * @param array<int, FieldEntity> $formFields
26 *
27 * @return array<string, string[]>
28 */
29 public static function collectUploadedUrls(
30 array $filesLookup,
31 array $decodedMap,
32 array $formFields
33 ): array {
34 /** @var array<string, string[]> $byLogicalKey */
35 $byLogicalKey = [];
36
37 foreach ($filesLookup as $paramName => $fileStruct) {
38 $logicalKey = SubmissionMultipartReader::resolveLogicalUploadKey(
39 (string) $paramName,
40 $decodedMap
41 );
42 if ($logicalKey === null || !is_array($fileStruct)) {
43 continue;
44 }
45
46 $url = self::processUploadedFile($fileStruct, $logicalKey, $formFields);
47 if ($url === null) {
48 continue;
49 }
50
51 if (!isset($byLogicalKey[$logicalKey])) {
52 $byLogicalKey[$logicalKey] = [];
53 }
54 $byLogicalKey[$logicalKey][] = $url;
55 }
56
57 return $byLogicalKey;
58 }
59
60 /**
61 * @param array<string, mixed> $fileStruct
62 * @param array<int, FieldEntity> $formFields
63 */
64 public static function processUploadedFile(
65 array $fileStruct,
66 string $logicalKey,
67 array $formFields
68 ): ?string {
69 if (($fileStruct['error'] ?? \UPLOAD_ERR_NO_FILE) === \UPLOAD_ERR_NO_FILE) {
70 return null;
71 }
72
73 SubmissionUploadFileValidator::assertUploadSucceeded($fileStruct, $logicalKey);
74 SubmissionUploadFileValidator::assertValidTmpFile($fileStruct, $logicalKey);
75
76 $size = isset($fileStruct['size']) ? (int) $fileStruct['size'] : 0;
77 $fieldEntity = SubmissionUploadFileValidator::findFieldForUploadKey($formFields, $logicalKey);
78 SubmissionUploadFileValidator::assertFileSizeWithinLimits($size, $fieldEntity, $logicalKey);
79 SubmissionUploadFileValidator::assertAllowedExtension($fileStruct, $fieldEntity, $logicalKey);
80
81 $result = self::storeUploadedFile($fileStruct, $fieldEntity);
82 if (isset($result['error'])) {
83 self::throwUploadFieldError($logicalKey, 'file_upload_failed');
84 }
85
86 return self::resolvePublicFileUrl($result, $fieldEntity, $logicalKey);
87 }
88
89 /**
90 * @param array<string, mixed> $fileStruct
91 *
92 * @return array<string, mixed>
93 */
94 private static function storeUploadedFile(array $fileStruct, ?FieldEntity $fieldEntity): array
95 {
96 $useIvyformsEntryDir = self::shouldUseIvyformsEntryDirectory($fieldEntity);
97
98 SubmissionUploadFiletypeFilter::beginContext($fileStruct, $fieldEntity);
99 add_filter(
100 SubmissionUploadFiletypeFilter::FILETYPE_FILTER,
101 [SubmissionUploadFiletypeFilter::class, 'filterFiletypeAndExt'],
102 10,
103 5
104 );
105
106 if ($useIvyformsEntryDir) {
107 SubmissionUploadDirectoryFilter::beginIvyformsEntryContext();
108 }
109
110 try {
111 $overrides = apply_filters(
112 'ivyforms/file-upload/wp_handle_upload_overrides',
113 ['test_form' => false],
114 $fileStruct,
115 $fieldEntity
116 );
117 if (!is_array($overrides)) {
118 $overrides = ['test_form' => false];
119 }
120
121 $tmp = isset($fileStruct['tmp_name']) ? (string) $fileStruct['tmp_name'] : '';
122 $useSideload = $tmp !== '' && !is_uploaded_file($tmp);
123
124 return $useSideload
125 ? wp_handle_sideload($fileStruct, $overrides)
126 : wp_handle_upload($fileStruct, $overrides);
127 } finally {
128 if ($useIvyformsEntryDir) {
129 SubmissionUploadDirectoryFilter::endContext();
130 }
131 remove_filter(
132 SubmissionUploadFiletypeFilter::FILETYPE_FILTER,
133 [SubmissionUploadFiletypeFilter::class, 'filterFiletypeAndExt'],
134 10
135 );
136 SubmissionUploadFiletypeFilter::endContext();
137 }
138 }
139
140 /**
141 * @param array<string, mixed> $result
142 */
143 private static function resolvePublicFileUrl(
144 array $result,
145 ?FieldEntity $fieldEntity,
146 string $logicalKey
147 ): string {
148 $url = isset($result['url']) ? (string) $result['url'] : '';
149 if ($url === '') {
150 self::throwUploadFieldError($logicalKey, 'file_upload_failed');
151 }
152
153 if (
154 !$fieldEntity instanceof FieldEntity
155 || $fieldEntity->getFieldAdvancedSettings()->getSaveUploadsTo() !== 'media_library'
156 || empty($result['file'])
157 ) {
158 return $url;
159 }
160
161 $mediaUrl = SubmissionUploadMediaRegistrar::register((string) $result['file']);
162
163 return $mediaUrl !== '' ? $mediaUrl : $url;
164 }
165
166 private static function shouldUseIvyformsEntryDirectory(?FieldEntity $fieldEntity): bool
167 {
168 if (!$fieldEntity instanceof FieldEntity) {
169 return false;
170 }
171
172 return $fieldEntity->getFieldAdvancedSettings()->getSaveUploadsTo() === 'ivyforms';
173 }
174
175 /**
176 * @throws FieldValidationException
177 */
178 private static function throwUploadFieldError(string $logicalKey, string $code): void
179 {
180 $strings = BackendStrings::getExceptionStrings();
181 $message = $strings[$code] ?? $strings['file_upload_failed'];
182
183 throw new FieldValidationException($message, [
184 $logicalKey => ['code' => $code],
185 ]);
186 }
187 }
188