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 / Media / ImageService.php

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

284 lines 8.5 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\Media;
4
5 use IvyForms\Services\Translations\BackendStrings;
6
7 /**
8 * Image Service
9 *
10 * Handles image processing and upload operations.
11 * Images are processed when forms/confirmations/notifications are saved.
12 */
13 class ImageService
14 {
15 /**
16 * Base upload directory
17 */
18 private const BASE_UPLOAD_DIR = 'ivyforms';
19
20 /**
21 * Process and upload base64 images found in content
22 *
23 * @param string $content Content that may contain base64 images
24 * @param string $folder Subfolder name (confirmation, notification, etc.). Defaults to 'temp' if not specified.
25 * @return string Content with base64 images replaced by uploaded URLs
26 */
27 public function processImagesInContent(string $content, string $folder = 'temp'): string
28 {
29 if (empty($content)) {
30 return $content;
31 }
32
33 // Find all base64 images in img src attributes
34 $pattern = '/<img[^>]+src=["\']data:image\/([^;]+);base64,([^"\']+)["\']/i';
35
36 $content = preg_replace_callback(
37 $pattern,
38 function ($matches) use ($folder) {
39 // IGNORE $extension from <img> tag
40 $base64Data = $matches[2];
41
42 // Upload the image (extension is always determined by MIME)
43 $result = $this->uploadBase64Image($base64Data, $folder);
44
45 if ($result['success'] && isset($result['url'])) {
46 // Replace base64 with uploaded URL
47 // Replace only the data:image/...;base64,... part
48 $original = $matches[0];
49 $replace = preg_replace(
50 '/src=["\']data:image\/[^;]+;base64,[^"\']+["\']/',
51 'src="' . $result['url'] . '"',
52 $original
53 );
54 return $replace;
55 }
56
57 // If upload failed, keep original
58 return $matches[0];
59 },
60 $content
61 );
62
63 return $content;
64 }
65
66 /**
67 * Upload base64 image to WordPress uploads directory
68 *
69 * @param string $base64Data Base64 encoded image data (without data: prefix)
70 * @param string $folder Subfolder name (confirmation, notification, etc.)
71 * @param string $extension (IGNORED, always determined by MIME)
72 * @param string $fileName Optional custom filename without extension
73 * @return array<string, bool|string> Array with 'success' bool and 'url' or 'error' keys
74 */
75 public function uploadBase64Image(
76 string $base64Data,
77 string $folder,
78 string $extension = 'png', // Ignored
79 string $fileName = ''
80 ): array {
81 // Validate base64 data
82 $decodedData = base64_decode($base64Data, true);
83 if ($decodedData === false) {
84 return [
85 'success' => false,
86 'error' => BackendStrings::getExceptionStrings()['invalid_base64_data'],
87 ];
88 }
89
90 // Verify and validate image
91 $validationResult = $this->validateImage($decodedData);
92 if (!$validationResult['success']) {
93 return $validationResult;
94 }
95
96 // Use extension derived from actual MIME type ONLY
97 $extension = $validationResult['extension'];
98
99 // Create upload directory if it doesn't exist
100 $uploadDir = $this->getUploadDirectory($folder);
101 if (!wp_mkdir_p($uploadDir)) {
102 return [
103 'success' => false,
104 'error' => BackendStrings::getExceptionStrings()['failed_create_upload_dir'],
105 ];
106 }
107
108 // Process and prepare filename
109 $fileName = $this->prepareFileName($fileName, $extension);
110
111 // Full file path and handle conflicts
112 $filePath = $uploadDir . '/' . $fileName;
113 $fileName = $this->resolveFileConflict($filePath, $fileName);
114 $filePath = $uploadDir . '/' . $fileName;
115
116 // Write file
117 if (!$this->writeImageFile($filePath, $decodedData)) {
118 return [
119 'success' => false,
120 'error' => BackendStrings::getExceptionStrings()['failed_write_file'],
121 ];
122 }
123
124 // Get full URL
125 $fullUrl = $this->getFullUrl($folder, $fileName);
126
127 return [
128 'success' => true,
129 'url' => $fullUrl,
130 'fileName' => $fileName,
131 'folder' => $folder,
132 ];
133 }
134
135 /**
136 * Validate image data
137 *
138 * @param string $decodedData
139 * @return array<string, bool|string>
140 */
141 private function validateImage(string $decodedData): array
142 {
143 $imageInfo = getimagesizefromstring($decodedData);
144 if ($imageInfo === false) {
145 return [
146 'success' => false,
147 'error' => BackendStrings::getExceptionStrings()['invalid_image_data'],
148 ];
149 }
150
151 $allowedMimeTypes = [
152 'image/jpeg' => 'jpg',
153 'image/png' => 'png',
154 'image/gif' => 'gif',
155 'image/webp' => 'webp',
156 ];
157
158 $detectedMimeType = $imageInfo['mime'];
159 if (!array_key_exists($detectedMimeType, $allowedMimeTypes)) {
160 return [
161 'success' => false,
162 'error' => BackendStrings::getExceptionStrings()['unsupported_image_type'] . $detectedMimeType,
163 ];
164 }
165
166 return ['success' => true, 'extension' => $allowedMimeTypes[$detectedMimeType]];
167 }
168
169 /**
170 * Prepare and sanitize filename
171 *
172 * @param string $fileName
173 * @param string $extension
174 * @return string
175 */
176 private function prepareFileName(string $fileName, string $extension): string
177 {
178 if (empty($fileName)) {
179 $fileName = 'image-' . time() . '-' . uniqid();
180 }
181
182 $fileName = sanitize_file_name($fileName);
183 if (empty($fileName)) {
184 $fileName = 'image-' . time() . '-' . uniqid();
185 }
186
187 if (strpos($fileName, '.') === false) {
188 $fileName .= '.' . $extension;
189 }
190
191 return $fileName;
192 }
193
194 /**
195 * Resolve file name conflicts by appending counter
196 *
197 * @param string $filePath
198 * @param string $fileName
199 * @return string
200 */
201 private function resolveFileConflict(string $filePath, string $fileName): string
202 {
203 if (!file_exists($filePath)) {
204 return $fileName;
205 }
206
207 $counter = 1;
208 $baseName = pathinfo($fileName, PATHINFO_FILENAME);
209 $ext = pathinfo($fileName, PATHINFO_EXTENSION);
210 $uploadDir = dirname($filePath);
211
212 while (file_exists($uploadDir . '/' . $fileName)) {
213 $fileName = $baseName . '-' . $counter . '.' . $ext;
214 $counter++;
215 }
216
217 return $fileName;
218 }
219
220 /**
221 * Write image file to disk with proper permissions
222 *
223 * @param string $filePath
224 * @param string $decodedData
225 * @return bool
226 */
227 private function writeImageFile(string $filePath, string $decodedData): bool
228 {
229 $bytesWritten = file_put_contents($filePath, $decodedData);
230 return $bytesWritten !== false;
231 }
232
233 /**
234 * Get the full upload directory path for a specific folder
235 *
236 * @param string $folder Subfolder name
237 * @return string Full directory path
238 */
239 private function getUploadDirectory(string $folder): string
240 {
241 $wpUploadDir = wp_upload_dir();
242 $baseDir = $wpUploadDir['basedir'];
243
244 // Sanitize folder name
245 $folder = sanitize_file_name($folder);
246
247 return $baseDir . '/' . self::BASE_UPLOAD_DIR . '/' . $folder;
248 }
249
250 /**
251 * Get the full URL to an uploaded image
252 *
253 * @param string $folder Subfolder name
254 * @param string $fileName Filename
255 * @return string Full URL
256 */
257 public function getFullUrl(string $folder, string $fileName): string
258 {
259 $wpUploadDir = wp_upload_dir();
260 $baseUrl = $wpUploadDir['baseurl'];
261
262 return $baseUrl . '/' . self::BASE_UPLOAD_DIR . '/' . $folder . '/' . $fileName;
263 }
264
265 /**
266 * Delete uploaded image
267 *
268 * @param string $folder Subfolder name
269 * @param string $fileName Filename
270 * @return bool True if deleted, false otherwise
271 */
272 public function deleteImage(string $folder, string $fileName): bool
273 {
274 $uploadDir = $this->getUploadDirectory($folder);
275 $filePath = $uploadDir . '/' . $fileName;
276
277 if (file_exists($filePath)) {
278 return unlink($filePath);
279 }
280
281 return false;
282 }
283 }
284