PluginProbe
The Innovative Form Builder – IvyForms / 0.9.1
The Innovative Form Builder – IvyForms v0.9.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 0.9.1, at backend/src/Services/Media/ImageService.php

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