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 +165 -23 1.10.52.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()),
@@ -76,15 +160,27 @@
76 160
77 161 private function resolveTicketId($request)
78 162 {
79 163 $ticketId = $request->getSafe('ticket_id', 'intval');
80 - 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;
81 176 }
82 177
83 178 private function resolvePerson($ticketId, Request $request)
84 179 {
85 - if ($request->get('is_agent') == 'yes') {
86 - return Helper::getCurrentAgent();
180 + $agent = Helper::getCurrentAgent();
181 + if ($agent) {
182 + return $agent;
87 183 }
88 184
89 185 if ($ticketId && Helper::isPublicSignedTicketEnabled()) {
90 186 $intendedTicketHash = $request->getSafe('intended_ticket_hash', 'sanitize_text_field');
@@ -90,9 +186,10 @@
90 186 $intendedTicketHash = $request->getSafe('intended_ticket_hash', 'sanitize_text_field');
91 187 if ($intendedTicketHash && $intendedTicketHash != 'undefined') {
92 188 $ticket = Ticket::with(['customer'])
93 189 ->where('hash', $intendedTicketHash)
94 - ->find($ticketId);
190 + ->wherePublicIdentifier($ticketId)
191 + ->first();
95 192
96 193 if ($ticket && $ticket->customer) {
97 194 return $ticket->customer;
98 195 }
@@ -122,9 +219,9 @@
122 219
123 220 private function createAttachmentRecords($uploadedFiles, $ticketId, $person, $imageType)
124 221 {
125 222 $attachments = [];
126 - $full_path = null;
223 + $directPasteUrl = null;
127 224
128 225 foreach ($uploadedFiles as $file) {
129 226 if (empty($file['file_path'])) continue;
130 227
@@ -141,15 +238,16 @@
141 238 'local_temp_path' => $file['file_path'],
142 239 ]
143 240 ];
144 241
145 - if($imageType == 'direct_paste'){
146 - $full_path = esc_url($file['url']);
147 - }
148 -
149 242 try {
150 243 $attachment = Attachment::create($fileData);
151 244 $attachments[] = $attachment->file_hash;
245 +
246 + if ($imageType == 'direct_paste') {
247 + $directPasteUrl = $attachment->secureUrl;
248 + }
249 +
152 250 do_action('fluent_support/attachment_uploaded_as_temp', $attachment, $ticketId);
153 251 $driver = Helper::getUploadDriverKey();
154 252
155 253 do_action_ref_array('fluent_support/attachment_uploaded_as_temp_' . $driver, [&$attachment, $ticketId]);
@@ -157,9 +255,9 @@
157 255 continue;
158 256 }
159 257 }
160 258
161 - return $imageType == 'direct_paste' ? $full_path : $attachments;
259 + return $imageType == 'direct_paste' ? $directPasteUrl : $attachments;
162 260 }
163 261
164 262 public function uploadImage(Request $request)
165 263 {
@@ -164,15 +262,23 @@
164 262 public function uploadImage(Request $request)
165 263 {
166 264 $images = $request->files();
167 265 $ticketId = $this->resolveTicketId($request);
168 - $this->isValidImageType($images);
169 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 +
170 276 try {
171 277 $uploadedFiles = UploadService::handleUploadToLocal($ticketId, $images);
172 278 } catch (\Exception $e) {
173 279 return $this->sendError([
174 - 'message' => $e->getMessage(),
280 + 'message' => Helper::getSafeErrorMessage($e),
175 281 ]);
176 282 }
177 283
178 284 return [
@@ -177,19 +283,55 @@
177 283
178 284 return [
179 285 'images' => $uploadedFiles,
180 286 ];
181 -
182 287 }
183 288
184 - private function isValidImageType($image)
289 + private function isValidImageType($images)
185 290 {
186 - $imageType = $image['image']->getClientOriginalExtension();
187 - $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 + }
188 296
189 - 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)) {
190 303 return $this->sendError([
191 - 'message' => 'Invalid image file.',
304 + 'message' => __('Invalid image file type.', 'fluent-support'),
192 305 ]);
193 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;
194 336 }
195 337 }