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 +277 -35 1.5.52.4.0 View file →
@@ -3,12 +3,13 @@
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\App\Services\Includes\FileSystem;
10 -use FluentSupport\Framework\Request\Request;
10 +use FluentSupport\Framework\Http\Request\Request;
11 +use FluentSupport\App\Services\Includes\UploadService;
11 12
12 13 /**
13 14 * UploaderController class is responsible for uploading file
14 15 * @package FluentSupport\App\Http\Controllers
@@ -24,72 +25,313 @@
24 25 * @throws \FluentSupport\Framework\Validator\ValidationException
25 26 */
26 27 public function uploadTicketFiles(Request $request)
27 28 {
28 - //get settings from settings table
29 29 $settings = (new Settings())->globalBusinessSettings();
30 - $maxFileSize = absint($settings['max_file_size']);
30 + $maxFileSize = floatval($settings['max_file_size']);
31 + $maxFileUpload = intval($settings['max_file_upload']);
31 32 $mimeHeadings = Helper::getAcceptedMimeHeadings();
33 + $maxSizeBytes = $maxFileSize * 1024;
34 + $imageType = $request->type ? $request->type : null;
32 35
33 - $maxSizeBytes = $maxFileSize * 1024;
36 + $files = $request->files();
34 37
35 - //Validate the file type and size
36 - $files = $this->validate($this->request->files(), [
37 - 'file' => 'max:' . $maxSizeBytes . '|mimetypes:' . implode(',', Helper::ticketAcceptedFileMiles())
38 - ], [
38 + if ($partsError = $this->rejectUnexpectedFileParts($files)) {
39 + return $partsError;
40 + }
41 +
42 + $ticketId = $this->resolveTicketId($request);
43 + $person = $this->resolvePerson($ticketId, $request);
44 +
45 + if ($permissionError = $this->checkPermissionToUploadFile($person)) {
46 + return $permissionError;
47 + }
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 +
59 + try {
60 + $uploadedFiles = UploadService::handleTempFileUpload($files);
61 + } catch (\Exception $e) {
62 + return $this->sendError([
63 + 'message' => Helper::getSafeErrorMessage($e),
64 + ]);
65 + }
66 +
67 + if (is_wp_error($uploadedFiles)) {
68 + return $this->sendError([
69 + 'message' => $uploadedFiles->get_error_message(),
70 + ]);
71 + }
72 +
73 + $attachmentHashes = $this->createAttachmentRecords($uploadedFiles, $ticketId, $person, $imageType);
74 +
75 + return [
76 + 'attachments' => $attachmentHashes,
77 + ];
78 + }
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 +
145 + private function validateUploadedFiles($files, $maxSizeBytes, $mimeHeadings, $maxFileSize)
146 + {
147 + $validationRules = [
148 + 'file' => 'max:' . $maxSizeBytes . '|mimetypes:' . implode(',', Helper::ticketAcceptedFileMiles()),
149 + ];
150 +
151 + $validationMessages = [
152 + // translators: %s is a comma-separated list of allowed file types (e.g., "jpg, png, pdf")
39 153 'file.mimetypes' => sprintf(__('Only %s files are allowed.', 'fluent-support'), implode(', ', $mimeHeadings)),
40 - 'file.max' => sprintf(__('The file can not be more than %dMB. Please upload somewhere like dropbox/google drive and paste the link in the response', 'fluent-support'), $maxFileSize)
41 - ]);
154 + // translators: %.01f is the maximum file size in megabytes
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),
156 + ];
42 157
158 + $this->validate($files, $validationRules, $validationMessages);
159 + }
43 160
44 - //get ticket by ticket id
45 - $ticketId = $request->get('ticket_id');
161 + private function resolveTicketId($request)
162 + {
163 + $ticketId = $request->getSafe('ticket_id', 'intval');
46 164
47 - if ($ticketId == 'undefined') {
48 - $ticketId = NULL;
165 + if ($ticketId == 'undefined' || !$ticketId) {
166 + return null;
49 167 }
50 168
51 - //Get customer or agent
52 - if ($ticketId && $request->get('intended_ticket_hash') && Helper::isPublicSignedTicketEnabled()) {
53 - $ticket = Ticket::with(['customer'])->findOrFail($ticketId);
54 - $person = $ticket->customer;
55 - } else {
56 - $person = Helper::getCurrentPerson();
169 + if (Helper::getCurrentAgent()) {
170 + return $ticketId;
57 171 }
58 172
59 - //Check if customer has permission to uipload file
60 - if ($person->person_type == 'customer') {
173 + $ticket = Ticket::wherePublicIdentifier($ticketId)->first();
174 +
175 + return $ticket ? $ticket->id : null;
176 + }
177 +
178 + private function resolvePerson($ticketId, Request $request)
179 + {
180 + $agent = Helper::getCurrentAgent();
181 + if ($agent) {
182 + return $agent;
183 + }
184 +
185 + if ($ticketId && Helper::isPublicSignedTicketEnabled()) {
186 + $intendedTicketHash = $request->getSafe('intended_ticket_hash', 'sanitize_text_field');
187 + if ($intendedTicketHash && $intendedTicketHash != 'undefined') {
188 + $ticket = Ticket::with(['customer'])
189 + ->where('hash', $intendedTicketHash)
190 + ->wherePublicIdentifier($ticketId)
191 + ->first();
192 +
193 + if ($ticket && $ticket->customer) {
194 + return $ticket->customer;
195 + }
196 + }
197 + }
198 +
199 + return Helper::getCurrentPerson();
200 + }
201 +
202 + private function checkPermissionToUploadFile($person)
203 + {
204 + if (!$person) {
205 + return $this->sendError([
206 + 'message' => __('You do not have permission to upload a file', 'fluent-support'),
207 + ]);
208 + }
209 +
210 + if ($person->person_type === 'customer') {
61 211 $disabledFields = apply_filters('fluent_support/disabled_ticket_fields', []);
62 212 if (in_array('file_upload', $disabledFields)) {
63 213 return $this->sendError([
64 - 'message' => __('You do not have permission to upload a file', 'fluent-support')
214 + 'message' => __('You do not have permission to upload a file', 'fluent-support'),
65 215 ]);
66 216 }
67 217 }
68 - //Move file into the directory
69 - $uploadedFiles = FileSystem::setSubDir('ticket_' . $ticketId)->put($files);
218 + }
70 219
220 + private function createAttachmentRecords($uploadedFiles, $ticketId, $person, $imageType)
221 + {
71 222 $attachments = [];
72 - //Create records in attachment table
223 + $directPasteUrl = null;
224 +
73 225 foreach ($uploadedFiles as $file) {
226 + if (empty($file['file_path'])) continue;
74 227
75 228 $fileData = [
76 - 'ticket_id' => $ticketId,
77 - 'person_id' => $person->id,
229 + 'ticket_id' => intval($ticketId) ?: NULL,
230 + 'person_id' => intval($person->id),
78 231 'file_type' => $file['type'],
79 232 'file_path' => $file['file_path'],
80 - 'full_url' => $file['url'],
81 - 'title' => $file['name'],
233 + 'full_url' => esc_url($file['url']),
234 + 'title' => sanitize_file_name($file['name']),
82 235 'driver' => 'local',
83 - 'status' => 'in-active'
236 + 'status' => 'in-active',
237 + 'settings' => [
238 + 'local_temp_path' => $file['file_path'],
239 + ]
84 240 ];
85 241
86 - $attachment = Attachment::create($fileData);
87 - $attachments[] = $attachment->file_hash;
242 + try {
243 + $attachment = Attachment::create($fileData);
244 + $attachments[] = $attachment->file_hash;
245 +
246 + if ($imageType == 'direct_paste') {
247 + $directPasteUrl = $attachment->secureUrl;
248 + }
249 +
250 + do_action('fluent_support/attachment_uploaded_as_temp', $attachment, $ticketId);
251 + $driver = Helper::getUploadDriverKey();
252 +
253 + do_action_ref_array('fluent_support/attachment_uploaded_as_temp_' . $driver, [&$attachment, $ticketId]);
254 + } catch (\Exception $exception) {
255 + continue;
256 + }
88 257 }
89 258
259 + return $imageType == 'direct_paste' ? $directPasteUrl : $attachments;
260 + }
261 +
262 + public function uploadImage(Request $request)
263 + {
264 + $images = $request->files();
265 + $ticketId = $this->resolveTicketId($request);
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 +
276 + try {
277 + $uploadedFiles = UploadService::handleUploadToLocal($ticketId, $images);
278 + } catch (\Exception $e) {
279 + return $this->sendError([
280 + 'message' => Helper::getSafeErrorMessage($e),
281 + ]);
282 + }
283 +
90 284 return [
91 - 'attachments' => $attachments
285 + 'images' => $uploadedFiles,
92 286 ];
287 + }
93 288
289 + private function isValidImageType($images)
290 + {
291 + if (empty($images['image'])) {
292 + return $this->sendError([
293 + 'message' => __('No image file provided.', 'fluent-support'),
294 + ]);
295 + }
296 +
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)) {
303 + return $this->sendError([
304 + 'message' => __('Invalid image file type.', 'fluent-support'),
305 + ]);
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;
94 336 }
95 337 }