PluginProbe
Fluent Support – Helpdesk & Customer Support Ticket System / trunk
Fluent Support – Helpdesk & Customer Support Ticket System vtrunk
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 trunk, at app/Http/Controllers/UploaderController.php

338 lines 11.1 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\Modules\PermissionManager;
8 use FluentSupport\App\Services\EmailNotification\Settings;
9 use FluentSupport\App\Services\Helper;
10 use FluentSupport\Framework\Http\Request\Request;
11 use FluentSupport\App\Services\Includes\UploadService;
12
13 /**
14 * UploaderController class is responsible for uploading file
15 * @package FluentSupport\App\Http\Controllers
16 *
17 * @version 1.0.0
18 */
19 class UploaderController extends Controller
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 */
27 public function uploadTicketFiles(Request $request)
28 {
29 $settings = (new Settings())->globalBusinessSettings();
30 $maxFileSize = floatval($settings['max_file_size']);
31 $maxFileUpload = intval($settings['max_file_upload']);
32 $mimeHeadings = Helper::getAcceptedMimeHeadings();
33 $maxSizeBytes = $maxFileSize * 1024;
34 $imageType = $request->type ? $request->type : null;
35
36 $files = $request->files();
37
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")
153 'file.mimetypes' => sprintf(__('Only %s files are allowed.', 'fluent-support'), implode(', ', $mimeHeadings)),
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 ];
157
158 $this->validate($files, $validationRules, $validationMessages);
159 }
160
161 private function resolveTicketId($request)
162 {
163 $ticketId = $request->getSafe('ticket_id', 'intval');
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;
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') {
211 $disabledFields = apply_filters('fluent_support/disabled_ticket_fields', []);
212 if (in_array('file_upload', $disabledFields)) {
213 return $this->sendError([
214 'message' => __('You do not have permission to upload a file', 'fluent-support'),
215 ]);
216 }
217 }
218 }
219
220 private function createAttachmentRecords($uploadedFiles, $ticketId, $person, $imageType)
221 {
222 $attachments = [];
223 $directPasteUrl = null;
224
225 foreach ($uploadedFiles as $file) {
226 if (empty($file['file_path'])) continue;
227
228 $fileData = [
229 'ticket_id' => intval($ticketId) ?: NULL,
230 'person_id' => intval($person->id),
231 'file_type' => $file['type'],
232 'file_path' => $file['file_path'],
233 'full_url' => esc_url($file['url']),
234 'title' => sanitize_file_name($file['name']),
235 'driver' => 'local',
236 'status' => 'in-active',
237 'settings' => [
238 'local_temp_path' => $file['file_path'],
239 ]
240 ];
241
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 }
257 }
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
284 return [
285 'images' => $uploadedFiles,
286 ];
287 }
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;
336 }
337 }
338