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 +290 -29 1.5.02.4.0 View file →
@@ -3,74 +3,335 @@
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
13 +/**
14 + * UploaderController class is responsible for uploading file
15 + * @package FluentSupport\App\Http\Controllers
16 + *
17 + * @version 1.0.0
18 + */
12 19 class UploaderController extends Controller
13 20 {
21 + /**
22 + * uploadTicketFiles method will upload all the attached file in a ticket
23 + * @param Request $request
24 + * @return array[]
25 + * @throws \FluentSupport\Framework\Validator\ValidationException
26 + */
14 27 public function uploadTicketFiles(Request $request)
15 28 {
16 29 $settings = (new Settings())->globalBusinessSettings();
17 - $maxFileSize = absint($settings['max_file_size']);
30 + $maxFileSize = floatval($settings['max_file_size']);
31 + $maxFileUpload = intval($settings['max_file_upload']);
18 32 $mimeHeadings = Helper::getAcceptedMimeHeadings();
33 + $maxSizeBytes = $maxFileSize * 1024;
34 + $imageType = $request->type ? $request->type : null;
19 35
20 - $maxSizeBytes = $maxFileSize * 1024;
36 + $files = $request->files();
21 37
22 - $files = $this->validate($this->request->files(), [
23 - 'file' => 'max:' . $maxSizeBytes . '|mimetypes:' . implode(',', Helper::ticketAcceptedFileMiles())
24 - ], [
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")
25 153 'file.mimetypes' => sprintf(__('Only %s files are allowed.', 'fluent-support'), implode(', ', $mimeHeadings)),
26 - '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)
27 - ]);
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 + ];
28 157
29 - $ticketId = $request->get('ticket_id');
158 + $this->validate($files, $validationRules, $validationMessages);
159 + }
30 160
31 - if ($ticketId == 'undefined') {
32 - $ticketId = NULL;
161 + private function resolveTicketId($request)
162 + {
163 + $ticketId = $request->getSafe('ticket_id', 'intval');
164 +
165 + if ($ticketId == 'undefined' || !$ticketId) {
166 + return null;
33 167 }
34 168
35 - if ($ticketId && $request->get('intended_ticket_hash') && Helper::isPublicSignedTicketEnabled()) {
36 - $ticket = Ticket::with(['customer'])->findOrFail($ticketId);
37 - $person = $ticket->customer;
38 - } else {
39 - $person = Helper::getCurrentPerson();
169 + if (Helper::getCurrentAgent()) {
170 + return $ticketId;
40 171 }
41 172
42 - 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') {
43 211 $disabledFields = apply_filters('fluent_support/disabled_ticket_fields', []);
44 212 if (in_array('file_upload', $disabledFields)) {
45 213 return $this->sendError([
46 - 'message' => 'You do not have permission to upload a file'
214 + 'message' => __('You do not have permission to upload a file', 'fluent-support'),
47 215 ]);
48 216 }
49 217 }
218 + }
50 219
51 - $uploadedFiles = FileSystem::setSubDir('ticket_' . $ticketId)->put($files);
220 + private function createAttachmentRecords($uploadedFiles, $ticketId, $person, $imageType)
221 + {
222 + $attachments = [];
223 + $directPasteUrl = null;
52 224
53 - $attachments = [];
54 225 foreach ($uploadedFiles as $file) {
226 + if (empty($file['file_path'])) continue;
55 227
56 228 $fileData = [
57 - 'ticket_id' => $ticketId,
58 - 'person_id' => $person->id,
229 + 'ticket_id' => intval($ticketId) ?: NULL,
230 + 'person_id' => intval($person->id),
59 231 'file_type' => $file['type'],
60 232 'file_path' => $file['file_path'],
61 - 'full_url' => $file['url'],
62 - 'title' => $file['name'],
233 + 'full_url' => esc_url($file['url']),
234 + 'title' => sanitize_file_name($file['name']),
63 235 'driver' => 'local',
64 - 'status' => 'in-active'
236 + 'status' => 'in-active',
237 + 'settings' => [
238 + 'local_temp_path' => $file['file_path'],
239 + ]
65 240 ];
66 241
67 - $attachment = Attachment::create($fileData);
68 - $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 + }
69 257 }
70 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 +
71 284 return [
72 - 'attachments' => $attachments
285 + 'images' => $uploadedFiles,
73 286 ];
287 + }
74 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;
75 336 }
76 337 }