PluginProbe
Fluent Support – Helpdesk & Customer Support Ticket System / 2.3.2
Fluent Support – Helpdesk & Customer Support Ticket System v2.3.2
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 1.5.6 All 67 releases
fluent-support / app / Http / Controllers / UploaderController.php

UploaderController.php in Fluent Support – Helpdesk & Customer Support Ticket System 2.3.2, at app/Http/Controllers/UploaderController.php

307 lines 10.2 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 namespace FluentSupport\App\Http\Controllers;
4
5 use FluentSupport\App\Models\Attachment;
6 use FluentSupport\App\Models\Ticket;
7 use FluentSupport\App\Services\EmailNotification\Settings;
8 use FluentSupport\App\Services\Helper;
9 use FluentSupport\Framework\Http\Request\Request;
10 use FluentSupport\App\Services\Includes\UploadService;
11
12 /**
13 * UploaderController class is responsible for uploading file
14 * @package FluentSupport\App\Http\Controllers
15 *
16 * @version 1.0.0
17 */
18 class UploaderController extends Controller
19 {
20 /**
21 * uploadTicketFiles method will upload all the attached file in a ticket
22 * @param Request $request
23 * @return array[]
24 * @throws \FluentSupport\Framework\Validator\ValidationException
25 */
26 public function uploadTicketFiles(Request $request)
27 {
28 $settings = (new Settings())->globalBusinessSettings();
29 $maxFileSize = floatval($settings['max_file_size']);
30 $maxFileUpload = intval($settings['max_file_upload']);
31 $mimeHeadings = Helper::getAcceptedMimeHeadings();
32 $maxSizeBytes = $maxFileSize * 1024;
33 $imageType = $request->type ? $request->type : null;
34
35 $files = $request->files();
36
37 if ($partsError = $this->rejectUnexpectedFileParts($files)) {
38 return $partsError;
39 }
40
41 $ticketId = $this->resolveTicketId($request);
42 $person = $this->resolvePerson($ticketId, $request);
43
44 if ($permissionError = $this->checkPermissionToUploadFile($person)) {
45 return $permissionError;
46 }
47
48 if ($quotaError = $this->checkAttachmentQuota($files, $person, $ticketId, $maxFileUpload)) {
49 return $quotaError;
50 }
51
52 $this->validateUploadedFiles($files, $maxSizeBytes, $mimeHeadings, $maxFileSize);
53
54 try {
55 $uploadedFiles = UploadService::handleTempFileUpload($files);
56 } catch (\Exception $e) {
57 return $this->sendError([
58 'message' => Helper::getSafeErrorMessage($e),
59 ]);
60 }
61
62 if (is_wp_error($uploadedFiles)) {
63 return $this->sendError([
64 'message' => $uploadedFiles->get_error_message(),
65 ]);
66 }
67
68 $attachmentHashes = $this->createAttachmentRecords($uploadedFiles, $ticketId, $person, $imageType);
69
70 return [
71 'attachments' => $attachmentHashes,
72 ];
73 }
74
75 /**
76 * Only the "file" multipart part is validated and processed downstream
77 * (UploadService/FileSystem::put() loops every top-level part it is given), so
78 * any other part name must be rejected here rather than silently passed through.
79 */
80 private function rejectUnexpectedFileParts($files)
81 {
82 $files = (array) $files;
83 $unexpectedKeys = array_diff(array_keys($files), ['file']);
84
85 if ($unexpectedKeys || empty($files['file'])) {
86 return $this->sendError([
87 'message' => __('Invalid file upload request.', 'fluent-support'),
88 ]);
89 }
90
91 return null;
92 }
93
94 private function checkAttachmentQuota($files, $person, $ticketId, $maxFileUpload)
95 {
96 if ($maxFileUpload <= 0) {
97 return null;
98 }
99
100 $newFiles = isset($files['file']) ? $files['file'] : null;
101 $newFilesCount = is_array($newFiles) ? count($newFiles) : 1;
102
103 $existingCount = Attachment::where('person_id', $person->id)
104 ->where('ticket_id', $ticketId)
105 ->where('status', 'in-active')
106 ->count();
107
108 if (($existingCount + $newFilesCount) > $maxFileUpload) {
109 return $this->sendError([
110 // translators: %d is the maximum number of files allowed per ticket
111 'message' => sprintf(__('You can upload a maximum of %d files.', 'fluent-support'), $maxFileUpload),
112 ]);
113 }
114
115 return null;
116 }
117
118 private function validateUploadedFiles($files, $maxSizeBytes, $mimeHeadings, $maxFileSize)
119 {
120 $validationRules = [
121 'file' => 'max:' . $maxSizeBytes . '|mimetypes:' . implode(',', Helper::ticketAcceptedFileMiles()),
122 ];
123
124 $validationMessages = [
125 // translators: %s is a comma-separated list of allowed file types (e.g., "jpg, png, pdf")
126 'file.mimetypes' => sprintf(__('Only %s files are allowed.', 'fluent-support'), implode(', ', $mimeHeadings)),
127 // translators: %.01f is the maximum file size in megabytes
128 '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),
129 ];
130
131 $this->validate($files, $validationRules, $validationMessages);
132 }
133
134 private function resolveTicketId($request)
135 {
136 $ticketId = $request->getSafe('ticket_id', 'intval');
137
138 if ($ticketId == 'undefined' || !$ticketId) {
139 return null;
140 }
141
142 if (Helper::getCurrentAgent()) {
143 return $ticketId;
144 }
145
146 $ticket = Ticket::wherePublicIdentifier($ticketId)->first();
147
148 return $ticket ? $ticket->id : null;
149 }
150
151 private function resolvePerson($ticketId, Request $request)
152 {
153 $agent = Helper::getCurrentAgent();
154 if ($agent) {
155 return $agent;
156 }
157
158 if ($ticketId && Helper::isPublicSignedTicketEnabled()) {
159 $intendedTicketHash = $request->getSafe('intended_ticket_hash', 'sanitize_text_field');
160 if ($intendedTicketHash && $intendedTicketHash != 'undefined') {
161 $ticket = Ticket::with(['customer'])
162 ->where('hash', $intendedTicketHash)
163 ->wherePublicIdentifier($ticketId)
164 ->first();
165
166 if ($ticket && $ticket->customer) {
167 return $ticket->customer;
168 }
169 }
170 }
171
172 return Helper::getCurrentPerson();
173 }
174
175 private function checkPermissionToUploadFile($person)
176 {
177 if (!$person) {
178 return $this->sendError([
179 'message' => __('You do not have permission to upload a file', 'fluent-support'),
180 ]);
181 }
182
183 if ($person->person_type === 'customer') {
184 $disabledFields = apply_filters('fluent_support/disabled_ticket_fields', []);
185 if (in_array('file_upload', $disabledFields)) {
186 return $this->sendError([
187 'message' => __('You do not have permission to upload a file', 'fluent-support'),
188 ]);
189 }
190 }
191 }
192
193 private function createAttachmentRecords($uploadedFiles, $ticketId, $person, $imageType)
194 {
195 $attachments = [];
196 $directPasteUrl = null;
197
198 foreach ($uploadedFiles as $file) {
199 if (empty($file['file_path'])) continue;
200
201 $fileData = [
202 'ticket_id' => intval($ticketId) ?: NULL,
203 'person_id' => intval($person->id),
204 'file_type' => $file['type'],
205 'file_path' => $file['file_path'],
206 'full_url' => esc_url($file['url']),
207 'title' => sanitize_file_name($file['name']),
208 'driver' => 'local',
209 'status' => 'in-active',
210 'settings' => [
211 'local_temp_path' => $file['file_path'],
212 ]
213 ];
214
215 try {
216 $attachment = Attachment::create($fileData);
217 $attachments[] = $attachment->file_hash;
218
219 if ($imageType == 'direct_paste') {
220 $directPasteUrl = $attachment->secureUrl;
221 }
222
223 do_action('fluent_support/attachment_uploaded_as_temp', $attachment, $ticketId);
224 $driver = Helper::getUploadDriverKey();
225
226 do_action_ref_array('fluent_support/attachment_uploaded_as_temp_' . $driver, [&$attachment, $ticketId]);
227 } catch (\Exception $exception) {
228 continue;
229 }
230 }
231
232 return $imageType == 'direct_paste' ? $directPasteUrl : $attachments;
233 }
234
235 public function uploadImage(Request $request)
236 {
237 $images = $request->files();
238 $ticketId = $this->resolveTicketId($request);
239
240 $validationError = $this->isValidImageType($images);
241 if ($validationError) {
242 return $validationError;
243 }
244
245 try {
246 $uploadedFiles = UploadService::handleUploadToLocal($ticketId, $images);
247 } catch (\Exception $e) {
248 return $this->sendError([
249 'message' => Helper::getSafeErrorMessage($e),
250 ]);
251 }
252
253 return [
254 'images' => $uploadedFiles,
255 ];
256 }
257
258 private function isValidImageType($images)
259 {
260 if (empty($images['image'])) {
261 return $this->sendError([
262 'message' => __('No image file provided.', 'fluent-support'),
263 ]);
264 }
265
266 $file = $images['image'];
267 $tempPath = $file->getPathname();
268 $extension = strtolower($file->getClientOriginalExtension());
269 $allowedExtensions = ['gif', 'ief', 'jpeg', 'jpg', 'webp', 'pjpeg', 'ktx', 'png'];
270
271 if (!in_array($extension, $allowedExtensions)) {
272 return $this->sendError([
273 'message' => __('Invalid image file type.', 'fluent-support'),
274 ]);
275 }
276
277 $allowedMimes = Helper::getMimeGroups()['images']['mimes'];
278 $realMime = $this->detectMimeType($tempPath);
279
280 if (!$realMime || !in_array($realMime, $allowedMimes)) {
281 return $this->sendError([
282 'message' => __('File content does not match the image type.', 'fluent-support'),
283 ]);
284 }
285
286 return null;
287 }
288
289 private function detectMimeType($filePath)
290 {
291 if (function_exists('finfo_open')) {
292 $finfo = finfo_open(FILEINFO_MIME_TYPE);
293 $mime = finfo_file($finfo, $filePath);
294 finfo_close($finfo);
295 return $mime;
296 }
297
298 if (function_exists('mime_content_type')) {
299 return mime_content_type($filePath);
300 }
301
302 // getimagesize works for standard image formats as last resort
303 $imageInfo = @getimagesize($filePath);
304 return $imageInfo ? $imageInfo['mime'] : false;
305 }
306 }
307