PluginProbe
Fluent Support – Helpdesk & Customer Support Ticket System / 2.4.0
Fluent Support – Helpdesk & Customer Support Ticket System v2.4.0
2.4.0 2.3.2 2.3.1 2.3.0 2.2.1 2.2.0 trunk 1.10.0 1.10.1 1.10.2 1.10.3 1.10.4 1.10.5 1.4.0 1.4.1 1.4.2 1.4.5 1.4.6 1.4.7 1.5.0 1.5.1 1.5.2 1.5.3 1.5.4 1.5.5 All 68 releases
← All changes | app/Http/Controllers/UploaderController.php +167 -23 1.10.02.4.0 View file →
@@ -3,11 +3,12 @@
3 3 namespace FluentSupport\App\Http\Controllers;
4 4
5 5 use FluentSupport\App\Models\Attachment;
6 6 use FluentSupport\App\Models\Ticket;
7 +use FluentSupport\App\Modules\PermissionManager;
7 8 use FluentSupport\App\Services\EmailNotification\Settings;
8 9 use FluentSupport\App\Services\Helper;
9 -use FluentSupport\Framework\Request\Request;
10 +use FluentSupport\Framework\Http\Request\Request;
10 11 use FluentSupport\App\Services\Includes\UploadService;
11 12
12 13 /**
13 14 * UploaderController class is responsible for uploading file
@@ -26,23 +27,41 @@
26 27 public function uploadTicketFiles(Request $request)
27 28 {
28 29 $settings = (new Settings())->globalBusinessSettings();
29 30 $maxFileSize = floatval($settings['max_file_size']);
31 + $maxFileUpload = intval($settings['max_file_upload']);
30 32 $mimeHeadings = Helper::getAcceptedMimeHeadings();
31 33 $maxSizeBytes = $maxFileSize * 1024;
32 34 $imageType = $request->type ? $request->type : null;
33 35
34 - $this->validateUploadedFiles($request->files(), $maxSizeBytes, $mimeHeadings, $maxFileSize);
36 + $files = $request->files();
37 +
38 + if ($partsError = $this->rejectUnexpectedFileParts($files)) {
39 + return $partsError;
40 + }
41 +
35 42 $ticketId = $this->resolveTicketId($request);
36 43 $person = $this->resolvePerson($ticketId, $request);
37 44
38 - $this->checkPermissionToUploadFile($person);
45 + if ($permissionError = $this->checkPermissionToUploadFile($person)) {
46 + return $permissionError;
47 + }
39 48
49 + if ($accessError = $this->checkTicketAccess($ticketId)) {
50 + return $accessError;
51 + }
52 +
53 + if ($quotaError = $this->checkAttachmentQuota($files, $person, $ticketId, $maxFileUpload)) {
54 + return $quotaError;
55 + }
56 +
57 + $this->validateUploadedFiles($files, $maxSizeBytes, $mimeHeadings, $maxFileSize);
58 +
40 59 try {
41 - $uploadedFiles = UploadService::handleTempFileUpload($request->files());
60 + $uploadedFiles = UploadService::handleTempFileUpload($files);
42 61 } catch (\Exception $e) {
43 62 return $this->sendError([
44 - 'message' => $e->getMessage(),
63 + 'message' => Helper::getSafeErrorMessage($e),
45 64 ]);
46 65 }
47 66
48 67 if (is_wp_error($uploadedFiles)) {
@@ -57,8 +76,73 @@
57 76 'attachments' => $attachmentHashes,
58 77 ];
59 78 }
60 79
80 + /**
81 + * Only the "file" multipart part is validated and processed downstream
82 + * (UploadService/FileSystem::put() loops every top-level part it is given), so
83 + * any other part name must be rejected here rather than silently passed through.
84 + */
85 + private function rejectUnexpectedFileParts($files)
86 + {
87 + $files = (array) $files;
88 + $unexpectedKeys = array_diff(array_keys($files), ['file']);
89 +
90 + if ($unexpectedKeys || empty($files['file'])) {
91 + return $this->sendError([
92 + 'message' => __('Invalid file upload request.', 'fluent-support'),
93 + ]);
94 + }
95 +
96 + return null;
97 + }
98 +
99 + /**
100 + * resolveTicketId() passes an agent's ticket_id through unchecked, so authorize it
101 + * before anything is written. No ticket id is legitimate — the Add Ticket form
102 + * uploads before the ticket exists.
103 + */
104 + private function checkTicketAccess($ticketId)
105 + {
106 + if (!$ticketId || !Helper::getCurrentAgent()) {
107 + return null;
108 + }
109 +
110 + $ticket = Ticket::find($ticketId);
111 +
112 + if (!$ticket || !PermissionManager::canAccessTicket($ticket)) {
113 + return $this->sendError([
114 + 'message' => __('You do not have permission to upload a file to this ticket', 'fluent-support'),
115 + ], 403);
116 + }
117 +
118 + return null;
119 + }
120 +
121 + private function checkAttachmentQuota($files, $person, $ticketId, $maxFileUpload)
122 + {
123 + if ($maxFileUpload <= 0) {
124 + return null;
125 + }
126 +
127 + $newFiles = isset($files['file']) ? $files['file'] : null;
128 + $newFilesCount = is_array($newFiles) ? count($newFiles) : 1;
129 +
130 + $existingCount = Attachment::where('person_id', $person->id)
131 + ->where('ticket_id', $ticketId)
132 + ->where('status', 'in-active')
133 + ->count();
134 +
135 + if (($existingCount + $newFilesCount) > $maxFileUpload) {
136 + return $this->sendError([
137 + // translators: %d is the maximum number of files allowed per ticket
138 + 'message' => sprintf(__('You can upload a maximum of %d files.', 'fluent-support'), $maxFileUpload),
139 + ]);
140 + }
141 +
142 + return null;
143 + }
144 +
61 145 private function validateUploadedFiles($files, $maxSizeBytes, $mimeHeadings, $maxFileSize)
62 146 {
63 147 $validationRules = [
64 148 'file' => 'max:' . $maxSizeBytes . '|mimetypes:' . implode(',', Helper::ticketAcceptedFileMiles()),
@@ -64,9 +148,11 @@
64 148 'file' => 'max:' . $maxSizeBytes . '|mimetypes:' . implode(',', Helper::ticketAcceptedFileMiles()),
65 149 ];
66 150
67 151 $validationMessages = [
152 + // translators: %s is a comma-separated list of allowed file types (e.g., "jpg, png, pdf")
68 153 'file.mimetypes' => sprintf(__('Only %s files are allowed.', 'fluent-support'), implode(', ', $mimeHeadings)),
154 + // translators: %.01f is the maximum file size in megabytes
69 155 'file.max' => sprintf(__('The file cannot be more than %.01fMB. Please upload somewhere like Dropbox/Google Drive and paste the link in the response', 'fluent-support'), $maxFileSize),
70 156 ];
71 157
72 158 $this->validate($files, $validationRules, $validationMessages);
@@ -74,15 +160,27 @@
74 160
75 161 private function resolveTicketId($request)
76 162 {
77 163 $ticketId = $request->getSafe('ticket_id', 'intval');
78 - return $ticketId == 'undefined' ? null : $ticketId;
164 +
165 + if ($ticketId == 'undefined' || !$ticketId) {
166 + return null;
167 + }
168 +
169 + if (Helper::getCurrentAgent()) {
170 + return $ticketId;
171 + }
172 +
173 + $ticket = Ticket::wherePublicIdentifier($ticketId)->first();
174 +
175 + return $ticket ? $ticket->id : null;
79 176 }
80 177
81 178 private function resolvePerson($ticketId, Request $request)
82 179 {
83 - if ($request->get('is_agent') == 'yes') {
84 - return Helper::getCurrentAgent();
180 + $agent = Helper::getCurrentAgent();
181 + if ($agent) {
182 + return $agent;
85 183 }
86 184
87 185 if ($ticketId && Helper::isPublicSignedTicketEnabled()) {
88 186 $intendedTicketHash = $request->getSafe('intended_ticket_hash', 'sanitize_text_field');
@@ -88,9 +186,10 @@
88 186 $intendedTicketHash = $request->getSafe('intended_ticket_hash', 'sanitize_text_field');
89 187 if ($intendedTicketHash && $intendedTicketHash != 'undefined') {
90 188 $ticket = Ticket::with(['customer'])
91 189 ->where('hash', $intendedTicketHash)
92 - ->find($ticketId);
190 + ->wherePublicIdentifier($ticketId)
191 + ->first();
93 192
94 193 if ($ticket && $ticket->customer) {
95 194 return $ticket->customer;
96 195 }
@@ -120,9 +219,9 @@
120 219
121 220 private function createAttachmentRecords($uploadedFiles, $ticketId, $person, $imageType)
122 221 {
123 222 $attachments = [];
124 - $full_path = null;
223 + $directPasteUrl = null;
125 224
126 225 foreach ($uploadedFiles as $file) {
127 226 if (empty($file['file_path'])) continue;
128 227
@@ -139,15 +238,16 @@
139 238 'local_temp_path' => $file['file_path'],
140 239 ]
141 240 ];
142 241
143 - if($imageType == 'direct_paste'){
144 - $full_path = esc_url($file['url']);
145 - }
146 -
147 242 try {
148 243 $attachment = Attachment::create($fileData);
149 244 $attachments[] = $attachment->file_hash;
245 +
246 + if ($imageType == 'direct_paste') {
247 + $directPasteUrl = $attachment->secureUrl;
248 + }
249 +
150 250 do_action('fluent_support/attachment_uploaded_as_temp', $attachment, $ticketId);
151 251 $driver = Helper::getUploadDriverKey();
152 252
153 253 do_action_ref_array('fluent_support/attachment_uploaded_as_temp_' . $driver, [&$attachment, $ticketId]);
@@ -155,9 +255,9 @@
155 255 continue;
156 256 }
157 257 }
158 258
159 - return $imageType == 'direct_paste' ? $full_path : $attachments;
259 + return $imageType == 'direct_paste' ? $directPasteUrl : $attachments;
160 260 }
161 261
162 262 public function uploadImage(Request $request)
163 263 {
@@ -162,15 +262,23 @@
162 262 public function uploadImage(Request $request)
163 263 {
164 264 $images = $request->files();
165 265 $ticketId = $this->resolveTicketId($request);
166 - $this->isValidImageType($images);
167 266
267 + if ($accessError = $this->checkTicketAccess($ticketId)) {
268 + return $accessError;
269 + }
270 +
271 + $validationError = $this->isValidImageType($images);
272 + if ($validationError) {
273 + return $validationError;
274 + }
275 +
168 276 try {
169 277 $uploadedFiles = UploadService::handleUploadToLocal($ticketId, $images);
170 278 } catch (\Exception $e) {
171 279 return $this->sendError([
172 - 'message' => $e->getMessage(),
280 + 'message' => Helper::getSafeErrorMessage($e),
173 281 ]);
174 282 }
175 283
176 284 return [
@@ -175,19 +283,55 @@
175 283
176 284 return [
177 285 'images' => $uploadedFiles,
178 286 ];
179 -
180 287 }
181 288
182 - private function isValidImageType($image)
289 + private function isValidImageType($images)
183 290 {
184 - $imageType = $image['image']->getClientOriginalExtension();
185 - $supportedTypes = ['gif', 'ief', 'jpeg', 'webp', 'pjpeg', 'ktx', 'png'];
291 + if (empty($images['image'])) {
292 + return $this->sendError([
293 + 'message' => __('No image file provided.', 'fluent-support'),
294 + ]);
295 + }
186 296
187 - if (! in_array($imageType, $supportedTypes)) {
297 + $file = $images['image'];
298 + $tempPath = $file->getPathname();
299 + $extension = strtolower($file->getClientOriginalExtension());
300 + $allowedExtensions = ['gif', 'ief', 'jpeg', 'jpg', 'webp', 'pjpeg', 'ktx', 'png'];
301 +
302 + if (!in_array($extension, $allowedExtensions)) {
188 303 return $this->sendError([
189 - 'message' => 'Invalid image file.',
304 + 'message' => __('Invalid image file type.', 'fluent-support'),
190 305 ]);
191 306 }
307 +
308 + $allowedMimes = Helper::getMimeGroups()['images']['mimes'];
309 + $realMime = $this->detectMimeType($tempPath);
310 +
311 + if (!$realMime || !in_array($realMime, $allowedMimes)) {
312 + return $this->sendError([
313 + 'message' => __('File content does not match the image type.', 'fluent-support'),
314 + ]);
315 + }
316 +
317 + return null;
318 + }
319 +
320 + private function detectMimeType($filePath)
321 + {
322 + if (function_exists('finfo_open')) {
323 + $finfo = finfo_open(FILEINFO_MIME_TYPE);
324 + $mime = finfo_file($finfo, $filePath);
325 + finfo_close($finfo);
326 + return $mime;
327 + }
328 +
329 + if (function_exists('mime_content_type')) {
330 + return mime_content_type($filePath);
331 + }
332 +
333 + // getimagesize works for standard image formats as last resort
334 + $imageInfo = @getimagesize($filePath);
335 + return $imageInfo ? $imageInfo['mime'] : false;
192 336 }
193 337 }