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 / SubmissionUploadFileValidator.php

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

235 lines 6.6 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\Common\Exceptions\ValidationException;
14 use IvyForms\Entity\Field\Field as FieldEntity;
15 use IvyForms\Services\Translations\BackendStrings;
16
17 /**
18 * Validates multipart file parts before they are stored.
19 */
20 final class SubmissionUploadFileValidator
21 {
22 /**
23 * @param array<string, mixed> $fileStruct
24 */
25 public static function assertUploadSucceeded(array $fileStruct, ?string $logicalKey = null): void
26 {
27 if (($fileStruct['error'] ?? \UPLOAD_ERR_OK) !== \UPLOAD_ERR_OK) {
28 self::fail('file_upload_failed', $logicalKey);
29 }
30 }
31
32 /**
33 * @param array<string, mixed> $fileStruct
34 */
35 public static function assertValidTmpFile(array $fileStruct, ?string $logicalKey = null): void
36 {
37 $tmp = isset($fileStruct['tmp_name']) ? (string) $fileStruct['tmp_name'] : '';
38 if ($tmp === '') {
39 self::fail('file_upload_failed', $logicalKey);
40 }
41
42 if (is_uploaded_file($tmp)) {
43 return;
44 }
45
46 // REST/multipart uploads may use a readable temp path without is_uploaded_file().
47 if (self::hasPositiveFileSize($tmp)) {
48 return;
49 }
50
51 self::fail('file_upload_failed', $logicalKey);
52 }
53
54 private static function hasPositiveFileSize(string $path): bool
55 {
56 if (!is_readable($path)) {
57 return false;
58 }
59
60 $size = filesize($path);
61
62 return $size !== false && $size > 0;
63 }
64
65 public static function assertFileSizeWithinLimits(
66 int $size,
67 ?FieldEntity $fieldEntity,
68 ?string $logicalKey = null
69 ): void {
70 $maxBytes = wp_max_upload_size();
71 if ($maxBytes > 0 && $size > $maxBytes) {
72 self::fail('file_upload_failed', $logicalKey);
73 }
74
75 if (!$fieldEntity instanceof FieldEntity) {
76 return;
77 }
78
79 $maxMb = $fieldEntity->getFieldAdvancedSettings()->getMaxFileSizeMb();
80 if ($maxMb <= 0 || $size <= $maxMb * 1024 * 1024) {
81 return;
82 }
83
84 self::fail('file_upload_failed', $logicalKey);
85 }
86
87 /**
88 * @param array<string, mixed> $fileStruct
89 */
90 public static function assertAllowedExtension(
91 array $fileStruct,
92 ?FieldEntity $fieldEntity,
93 ?string $logicalKey = null
94 ): void {
95 if (!$fieldEntity instanceof FieldEntity) {
96 return;
97 }
98
99 $uploadName = isset($fileStruct['name']) ? (string) $fileStruct['name'] : '';
100 $ext = $uploadName !== '' ? strtolower(pathinfo($uploadName, PATHINFO_EXTENSION)) : '';
101
102 /**
103 * @param string $resolved
104 * @param array<string, mixed> $fileStruct
105 * @param FieldEntity $fieldEntity
106 */
107 $ext = (string) apply_filters(
108 'ivyforms/file-upload/resolve_extension',
109 $ext,
110 $fileStruct,
111 $fieldEntity
112 );
113
114 $allowed = $fieldEntity->getFieldAdvancedSettings()->getAllowedFileExtensions();
115 $allowed = self::normalizeAllowedExtensions($allowed);
116 $allowed = apply_filters('ivyforms/file-upload/allowed_extensions', $allowed, $fieldEntity);
117 $allowed = self::normalizeAllowedExtensions($allowed);
118
119 if ($allowed === []) {
120 if (self::isExtensionAllowedByWordPressMimes($uploadName, $ext)) {
121 return;
122 }
123
124 self::fail('file_upload_invalid_type', $logicalKey);
125 }
126
127 if ($ext !== '' && in_array($ext, $allowed, true)) {
128 return;
129 }
130
131 self::fail('file_upload_invalid_type', $logicalKey);
132 }
133
134 private static function isExtensionAllowedByWordPressMimes(string $uploadName, string $ext): bool
135 {
136 if ($ext === '') {
137 return false;
138 }
139
140 $checked = wp_check_filetype($uploadName);
141
142 return $checked['ext'] !== false
143 && strtolower($checked['ext']) === $ext;
144 }
145
146 /**
147 * @param array<int, mixed> $allowed
148 *
149 * @return array<int, string>
150 */
151 public static function normalizeAllowedExtensions(array $allowed): array
152 {
153 $out = [];
154 $seen = [];
155 foreach ($allowed as $item) {
156 if (!is_string($item)) {
157 continue;
158 }
159 $ext = strtolower(ltrim(trim($item), '.'));
160 if ($ext === '' || isset($seen[$ext])) {
161 continue;
162 }
163 $seen[$ext] = true;
164 $out[] = $ext;
165 }
166
167 return $out;
168 }
169
170 public static function assertUploadCountWithinLimits(
171 int $count,
172 ?FieldEntity $fieldEntity,
173 ?string $logicalKey = null
174 ): void {
175 if (!$fieldEntity instanceof FieldEntity || $count === 0) {
176 return;
177 }
178
179 /** @var array{allowMultiple: bool, maxUploadCount: int} $limits */
180 $limits = apply_filters(
181 'ivyforms/file-upload/upload_limits',
182 [
183 'allowMultiple' => false,
184 'maxUploadCount' => 1,
185 ],
186 $fieldEntity
187 );
188
189 if (!$limits['allowMultiple'] && $count > 1) {
190 self::fail('file_upload_too_many_files', $logicalKey);
191 }
192
193 $maxCount = $limits['maxUploadCount'];
194 if ($limits['allowMultiple'] && $maxCount > 0 && $count > $maxCount) {
195 self::fail('file_upload_too_many_files', $logicalKey);
196 }
197 }
198
199 /**
200 * @param array<int, FieldEntity> $formFields
201 */
202 public static function findFieldForUploadKey(array $formFields, string $logicalKey): ?FieldEntity
203 {
204 if (!preg_match('/^file-upload_(\d+)$/', $logicalKey, $matches)) {
205 return null;
206 }
207 $idx = (int) $matches[1];
208 foreach ($formFields as $field) {
209 if ($field->getType() === 'file-upload' && $field->getIndex() === $idx) {
210 return $field;
211 }
212 }
213
214 return null;
215 }
216
217 /**
218 * @throws ValidationException
219 * @throws FieldValidationException
220 */
221 private static function fail(string $code, ?string $logicalKey = null): void
222 {
223 $strings = BackendStrings::getExceptionStrings();
224 $message = $strings[$code] ?? $strings['file_upload_failed'];
225
226 if ($logicalKey !== null && $logicalKey !== '') {
227 throw new FieldValidationException($message, [
228 $logicalKey => ['code' => $code],
229 ]);
230 }
231
232 throw new ValidationException($message);
233 }
234 }
235